diff options
| -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" |