summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dev/css-to-json-util.js12
-rw-r--r--dev/data/structured-content-overrides.css7
-rw-r--r--ext/css/material.css1
-rw-r--r--ext/css/structured-content.css15
-rw-r--r--ext/data/schemas/dictionary-term-bank-v3-schema.json23
-rw-r--r--ext/data/structured-content-style.json6
-rw-r--r--ext/images/external-link.svg1
-rw-r--r--ext/js/display/display-content-manager.js46
-rw-r--r--ext/js/display/display.js9
-rw-r--r--ext/js/display/sandbox/structured-content-generator.js28
-rw-r--r--ext/js/templates/sandbox/anki-template-renderer-content-manager.js10
-rw-r--r--resources/icons.svg24
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_bank_1.json25
13 files changed, 192 insertions, 15 deletions
diff --git a/dev/css-to-json-util.js b/dev/css-to-json-util.js
index d6a9622c..71033f3a 100644
--- a/dev/css-to-json-util.js
+++ b/dev/css-to-json-util.js
@@ -90,8 +90,9 @@ function generateRules(cssFile, overridesCssFile) {
const stylesheet1 = css.parse(content1, {}).stylesheet;
const stylesheet2 = css.parse(content2, {}).stylesheet;
- const removePropertyPattern = /^remove-property\s+([a-zA-Z0-9-]+)$/;
+ const removePropertyPattern = /^remove-property\s+([\w\W]+)$/;
const removeRulePattern = /^remove-rule$/;
+ const propertySeparator = /\s+/;
const rules = [];
@@ -139,10 +140,11 @@ function generateRules(cssFile, overridesCssFile) {
const comment = declaration.comment.trim();
let m;
if ((m = removePropertyPattern.exec(comment)) !== null) {
- const property = m[1];
- const removeCount = removeProperty(rules[index].styles, property, removedProperties);
- if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
- } else if ((m = removeRulePattern.exec(comment)) !== null) {
+ for (const property of m[1].split(propertySeparator)) {
+ const removeCount = removeProperty(rules[index].styles, property, removedProperties);
+ if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
+ }
+ } else if (removeRulePattern.test(comment)) {
rules.splice(index, 1);
}
}
diff --git a/dev/data/structured-content-overrides.css b/dev/data/structured-content-overrides.css
index 1d2ed830..9fd08f8f 100644
--- a/dev/data/structured-content-overrides.css
+++ b/dev/data/structured-content-overrides.css
@@ -54,3 +54,10 @@
border-width: 1px;
border-color: currentColor;
}
+.gloss-link-text {
+ /* remove-rule */
+}
+.gloss-link-external-icon {
+ display: none;
+ /* remove-property background-color vertical-align width height margin-left background-color position */
+}
diff --git a/ext/css/material.css b/ext/css/material.css
index 55ed13e6..4d8eda51 100644
--- a/ext/css/material.css
+++ b/ext/css/material.css
@@ -275,6 +275,7 @@ body {
.icon[data-icon=tag] { --icon-image: url(/images/tag.svg); }
.icon[data-icon=accessibility] { --icon-image: url(/images/accessibility.svg); }
.icon[data-icon=connection] { --icon-image: url(/images/connection.svg); }
+.icon[data-icon=external-link] { --icon-image: url(/images/external-link.svg); }
.icon[data-icon=material-down-arrow] {
--icon-image: url(/images/material-down-arrow.svg);
--icon-size: var(--material-arrow-dimension2) var(--material-arrow-dimension1);
diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css
index 092d9e7c..485527e5 100644
--- a/ext/css/structured-content.css
+++ b/ext/css/structured-content.css
@@ -198,6 +198,21 @@
}
+/* Links */
+.gloss-link-text {
+ vertical-align: middle;
+}
+.gloss-link-external-icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: calc(16em / var(--font-size-no-units));
+ height: calc(16em / var(--font-size-no-units));
+ margin-left: 0.25em;
+ background-color: var(--link-color);
+ position: relative;
+}
+
+
/* Structured content glossary styles */
.gloss-sc-table-container {
display: block;
diff --git a/ext/data/schemas/dictionary-term-bank-v3-schema.json b/ext/data/schemas/dictionary-term-bank-v3-schema.json
index 9898a15e..268a2c11 100644
--- a/ext/data/schemas/dictionary-term-bank-v3-schema.json
+++ b/ext/data/schemas/dictionary-term-bank-v3-schema.json
@@ -185,6 +185,29 @@
"enum": ["px", "em"]
}
}
+ },
+ {
+ "type": "object",
+ "description": "Link tag.",
+ "required": [
+ "tag",
+ "href"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "tag": {
+ "type": "string",
+ "const": "a"
+ },
+ "content": {
+ "$ref": "#/definitions/structuredContent"
+ },
+ "href": {
+ "type": "string",
+ "description": "The URL for the link. URLs starting with a ? are treated as internal links to other dictionary content.",
+ "pattern": "^(?:https?:|\\?)[\\w\\W]*"
+ }
+ }
}
]
}
diff --git a/ext/data/structured-content-style.json b/ext/data/structured-content-style.json
index ae216abd..fe222486 100644
--- a/ext/data/structured-content-style.json
+++ b/ext/data/structured-content-style.json
@@ -294,6 +294,12 @@
]
},
{
+ "selectors": [".gloss-link-external-icon"],
+ "styles": [
+ ["display", "none"]
+ ]
+ },
+ {
"selectors": [".gloss-sc-table-container"],
"styles": [
["display", "block"]
diff --git a/ext/images/external-link.svg b/ext/images/external-link.svg
new file mode 100644
index 00000000..f8bbba58
--- /dev/null
+++ b/ext/images/external-link.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M8.25 2l2.25 2-4 4L8 9.5l4-4 2 2.25V2H8.25zM2 4v10h10V7l-2 2v3H4V6h3l2-2H2z"/></svg> \ No newline at end of file
diff --git a/ext/js/display/display-content-manager.js b/ext/js/display/display-content-manager.js
index 0818f915..0b91b40c 100644
--- a/ext/js/display/display-content-manager.js
+++ b/ext/js/display/display-content-manager.js
@@ -37,11 +37,14 @@
class DisplayContentManager {
/**
* Creates a new instance of the class.
+ * @param {Display} display The display instance that owns this object.
*/
- constructor() {
+ constructor(display) {
+ this._display = display;
this._token = {};
this._mediaCache = new Map();
this._loadMediaData = [];
+ this._eventListeners = new EventListenerCollection();
}
/**
@@ -77,6 +80,23 @@ class DisplayContentManager {
this._mediaCache.clear();
this._token = {};
+
+ this._eventListeners.removeAllEventListeners();
+ }
+
+ /**
+ * Sets up attributes and events for a link element.
+ * @param {Element} element The link element.
+ * @param {string} href The URL.
+ * @param {boolean} internal Whether or not the URL is an internal or external link.
+ */
+ prepareLink(element, href, internal) {
+ element.href = href;
+ if (!internal) {
+ element.target = '_blank';
+ element.rel = 'noreferrer noopener';
+ }
+ this._eventListeners.addEventListener(element, 'click', this._onLinkClick.bind(this));
}
async _loadMedia(path, dictionary, onLoad, onUnload) {
@@ -127,4 +147,28 @@ class DisplayContentManager {
}
return cachedData;
}
+
+ _onLinkClick(e) {
+ const {href} = e.currentTarget;
+ if (typeof href !== 'string') { return; }
+
+ const baseUrl = new URL(location.href);
+ const url = new URL(href, baseUrl);
+ const internal = (url.protocol === baseUrl.protocol && url.host === baseUrl.host);
+ if (!internal) { return; }
+
+ e.preventDefault();
+
+ const params = {};
+ for (const [key, value] of url.searchParams.entries()) {
+ params[key] = value;
+ }
+ this._display.setContent({
+ historyMode: 'new',
+ focus: false,
+ params,
+ state: null,
+ content: null
+ });
+ }
}
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index a89008b4..02d8513f 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -52,7 +52,7 @@ class Display extends EventDispatcher {
this._styleNode = null;
this._eventListeners = new EventListenerCollection();
this._setContentToken = null;
- this._contentManager = new DisplayContentManager();
+ this._contentManager = new DisplayContentManager(this);
this._hotkeyHelpController = new HotkeyHelpController();
this._displayGenerator = new DisplayGenerator({
japaneseUtil,
@@ -938,7 +938,7 @@ class Display extends EventDispatcher {
this._dictionaryEntries = dictionaryEntries;
- this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
+ this._updateNavigationAuto();
this._setNoContentVisible(dictionaryEntries.length === 0 && lookup);
const container = this._container;
@@ -1002,6 +1002,7 @@ class Display extends EventDispatcher {
_clearContent() {
this._container.textContent = '';
+ this._updateNavigationAuto();
this._setQuery('', '', 0);
this._triggerContentUpdateStart();
@@ -1058,6 +1059,10 @@ class Display extends EventDispatcher {
document.title = title;
}
+ _updateNavigationAuto() {
+ this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
+ }
+
_updateNavigation(previous, next) {
const {documentElement} = document;
if (documentElement !== null) {
diff --git a/ext/js/display/sandbox/structured-content-generator.js b/ext/js/display/sandbox/structured-content-generator.js
index 799da586..eb847d07 100644
--- a/ext/js/display/sandbox/structured-content-generator.js
+++ b/ext/js/display/sandbox/structured-content-generator.js
@@ -59,6 +59,8 @@ class StructuredContentGenerator {
return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
case 'img':
return this.createDefinitionImage(content, dictionary);
+ case 'a':
+ return this._createLinkElement(content, dictionary);
}
return null;
}
@@ -253,4 +255,30 @@ class StructuredContentGenerator {
if (typeof marginRight === 'number') { style.marginRight = `${marginRight}em`; }
if (typeof marginBottom === 'number') { style.marginBottom = `${marginBottom}em`; }
}
+
+ _createLinkElement(content, dictionary) {
+ let {href} = content;
+ const internal = href.startsWith('?');
+ if (internal) {
+ href = `${location.protocol}//${location.host}/search.html${href.length > 1 ? href : ''}`;
+ }
+
+ const node = this._createElement('a', 'gloss-link');
+ node.dataset.external = `${!internal}`;
+
+ const text = this._createElement('span', 'gloss-link-text');
+ node.appendChild(text);
+
+ const child = this.createStructuredContent(content.content, dictionary);
+ if (child !== null) { text.appendChild(child); }
+
+ if (!internal) {
+ const icon = this._createElement('span', 'gloss-link-external-icon icon');
+ icon.dataset.icon = 'external-link';
+ node.appendChild(icon);
+ }
+
+ this._contentManager.prepareLink(node, href, internal);
+ return node;
+ }
}
diff --git a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
index a2573e09..0dabd86e 100644
--- a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
+++ b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
@@ -69,4 +69,14 @@ class AnkiTemplateRendererContentManager {
}
this._onUnloadCallbacks = [];
}
+
+ /**
+ * Sets up attributes and events for a link element.
+ * @param {Element} element The link element.
+ * @param {string} href The URL.
+ * @param {boolean} internal Whether or not the URL is an internal or external link.
+ */
+ prepareLink(element, href, internal) {
+ element.href = internal ? '#' : href;
+ }
}
diff --git a/resources/icons.svg b/resources/icons.svg
index b6bdf62e..c3e37ad6 100644
--- a/resources/icons.svg
+++ b/resources/icons.svg
@@ -25,11 +25,11 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="32"
- inkscape:cx="4.203125"
- inkscape:cy="5.734375"
+ inkscape:zoom="16"
+ inkscape:cx="-1.03125"
+ inkscape:cy="3.84375"
inkscape:document-units="px"
- inkscape:current-layer="layer53"
+ inkscape:current-layer="layer54"
showgrid="true"
units="px"
inkscape:snap-center="true"
@@ -566,7 +566,6 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
@@ -1650,8 +1649,9 @@
</g>
<g
inkscape:groupmode="layer"
- id="layer53"
- inkscape:label="Connection">
+ id="g268"
+ inkscape:label="Connection"
+ style="display:none">
<g
id="g1369"
transform="matrix(0,-1,-1,0,16,16)">
@@ -1663,4 +1663,14 @@
sodipodi:nodetypes="ssccccsssscssssccccsssscssssssssssssssssss" />
</g>
</g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer54"
+ inkscape:label="External Link"
+ style="display:inline">
+ <path
+ id="rect1109"
+ style="fill:#000000;stroke-linecap:round;stroke-opacity:0.387097;fill-opacity:1"
+ d="M 8.25 2 L 10.5 4 L 6.5 8 L 8 9.5 L 12 5.5 L 14 7.75 L 14 2 L 8.25 2 z M 2 4 L 2 14 L 12 14 L 12 7 L 10 9 L 10 12 L 4 12 L 4 6 L 7 6 L 9 4 L 2 4 z " />
+ </g>
</svg>
diff --git a/test/data/dictionaries/valid-dictionary1/term_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
index 0d94d6d1..e7506255 100644
--- a/test/data/dictionaries/valid-dictionary1/term_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/term_bank_1.json
@@ -179,6 +179,31 @@
]
},
" text 3"
+ ]},
+ {"type": "structured-content", "content": [
+ {
+ "tag": "a",
+ "href": "?",
+ "content": [
+ "internal link 1"
+ ]
+ },
+ " ",
+ {
+ "tag": "a",
+ "href": "?query=よみ&wildcards=off",
+ "content": [
+ "internal link 2"
+ ]
+ },
+ " ",
+ {
+ "tag": "a",
+ "href": "https://foosoft.net/projects/yomichan/",
+ "content": [
+ "external link"
+ ]
+ }
]}
],
100, "P E1"