diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2022-03-17 19:01:59 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-17 19:01:59 -0400 |
commit | 7a2ab866099edffaba471ad808593f67ee796b21 (patch) | |
tree | ddfe746ed76e16d80e0ac6e3029e2bc1049544d2 | |
parent | 8aa060337cea2bb8fce7864d509d07df4688f1c2 (diff) |
Structured content links (#2089)
* Update CSS to JSON converter to generalize the remove-property comment
* Fix navigation not being updated when _clearContent is run
* Add structured content schema for link tags
* Add test links
* Add external-link icon
* Pass Display instance to DisplayContentManager
* Update structured content generation
* Update link styles
-rw-r--r-- | dev/css-to-json-util.js | 12 | ||||
-rw-r--r-- | dev/data/structured-content-overrides.css | 7 | ||||
-rw-r--r-- | ext/css/material.css | 1 | ||||
-rw-r--r-- | ext/css/structured-content.css | 15 | ||||
-rw-r--r-- | ext/data/schemas/dictionary-term-bank-v3-schema.json | 23 | ||||
-rw-r--r-- | ext/data/structured-content-style.json | 6 | ||||
-rw-r--r-- | ext/images/external-link.svg | 1 | ||||
-rw-r--r-- | ext/js/display/display-content-manager.js | 46 | ||||
-rw-r--r-- | ext/js/display/display.js | 9 | ||||
-rw-r--r-- | ext/js/display/sandbox/structured-content-generator.js | 28 | ||||
-rw-r--r-- | ext/js/templates/sandbox/anki-template-renderer-content-manager.js | 10 | ||||
-rw-r--r-- | resources/icons.svg | 24 | ||||
-rw-r--r-- | test/data/dictionaries/valid-dictionary1/term_bank_1.json | 25 |
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" |