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
This commit is contained in:
toasted-nutbread 2022-03-17 19:01:59 -04:00 committed by GitHub
parent 8aa060337c
commit 7a2ab86609
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 192 additions and 15 deletions

View File

@ -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);
}
}

View File

@ -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 */
}

View File

@ -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);

View File

@ -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;

View File

@ -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]*"
}
}
}
]
}

View File

@ -293,6 +293,12 @@
["display", "inline"]
]
},
{
"selectors": [".gloss-link-external-icon"],
"styles": [
["display", "none"]
]
},
{
"selectors": [".gloss-sc-table-container"],
"styles": [

View File

@ -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>

After

Width:  |  Height:  |  Size: 194 B

View File

@ -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
});
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@ -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"