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:
parent
8aa060337c
commit
7a2ab86609
@ -90,8 +90,9 @@ function generateRules(cssFile, overridesCssFile) {
|
|||||||
const stylesheet1 = css.parse(content1, {}).stylesheet;
|
const stylesheet1 = css.parse(content1, {}).stylesheet;
|
||||||
const stylesheet2 = css.parse(content2, {}).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 removeRulePattern = /^remove-rule$/;
|
||||||
|
const propertySeparator = /\s+/;
|
||||||
|
|
||||||
const rules = [];
|
const rules = [];
|
||||||
|
|
||||||
@ -139,10 +140,11 @@ function generateRules(cssFile, overridesCssFile) {
|
|||||||
const comment = declaration.comment.trim();
|
const comment = declaration.comment.trim();
|
||||||
let m;
|
let m;
|
||||||
if ((m = removePropertyPattern.exec(comment)) !== null) {
|
if ((m = removePropertyPattern.exec(comment)) !== null) {
|
||||||
const property = m[1];
|
for (const property of m[1].split(propertySeparator)) {
|
||||||
const removeCount = removeProperty(rules[index].styles, property, removedProperties);
|
const removeCount = removeProperty(rules[index].styles, property, removedProperties);
|
||||||
if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
|
if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
|
||||||
} else if ((m = removeRulePattern.exec(comment)) !== null) {
|
}
|
||||||
|
} else if (removeRulePattern.test(comment)) {
|
||||||
rules.splice(index, 1);
|
rules.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,3 +54,10 @@
|
|||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: currentColor;
|
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 */
|
||||||
|
}
|
||||||
|
@ -275,6 +275,7 @@ body {
|
|||||||
.icon[data-icon=tag] { --icon-image: url(/images/tag.svg); }
|
.icon[data-icon=tag] { --icon-image: url(/images/tag.svg); }
|
||||||
.icon[data-icon=accessibility] { --icon-image: url(/images/accessibility.svg); }
|
.icon[data-icon=accessibility] { --icon-image: url(/images/accessibility.svg); }
|
||||||
.icon[data-icon=connection] { --icon-image: url(/images/connection.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[data-icon=material-down-arrow] {
|
||||||
--icon-image: url(/images/material-down-arrow.svg);
|
--icon-image: url(/images/material-down-arrow.svg);
|
||||||
--icon-size: var(--material-arrow-dimension2) var(--material-arrow-dimension1);
|
--icon-size: var(--material-arrow-dimension2) var(--material-arrow-dimension1);
|
||||||
|
@ -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 */
|
/* Structured content glossary styles */
|
||||||
.gloss-sc-table-container {
|
.gloss-sc-table-container {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -185,6 +185,29 @@
|
|||||||
"enum": ["px", "em"]
|
"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]*"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -293,6 +293,12 @@
|
|||||||
["display", "inline"]
|
["display", "inline"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"selectors": [".gloss-link-external-icon"],
|
||||||
|
"styles": [
|
||||||
|
["display", "none"]
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"selectors": [".gloss-sc-table-container"],
|
"selectors": [".gloss-sc-table-container"],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
1
ext/images/external-link.svg
Normal file
1
ext/images/external-link.svg
Normal 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 |
@ -37,11 +37,14 @@
|
|||||||
class DisplayContentManager {
|
class DisplayContentManager {
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the class.
|
* 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._token = {};
|
||||||
this._mediaCache = new Map();
|
this._mediaCache = new Map();
|
||||||
this._loadMediaData = [];
|
this._loadMediaData = [];
|
||||||
|
this._eventListeners = new EventListenerCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +80,23 @@ class DisplayContentManager {
|
|||||||
this._mediaCache.clear();
|
this._mediaCache.clear();
|
||||||
|
|
||||||
this._token = {};
|
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) {
|
async _loadMedia(path, dictionary, onLoad, onUnload) {
|
||||||
@ -127,4 +147,28 @@ class DisplayContentManager {
|
|||||||
}
|
}
|
||||||
return cachedData;
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class Display extends EventDispatcher {
|
|||||||
this._styleNode = null;
|
this._styleNode = null;
|
||||||
this._eventListeners = new EventListenerCollection();
|
this._eventListeners = new EventListenerCollection();
|
||||||
this._setContentToken = null;
|
this._setContentToken = null;
|
||||||
this._contentManager = new DisplayContentManager();
|
this._contentManager = new DisplayContentManager(this);
|
||||||
this._hotkeyHelpController = new HotkeyHelpController();
|
this._hotkeyHelpController = new HotkeyHelpController();
|
||||||
this._displayGenerator = new DisplayGenerator({
|
this._displayGenerator = new DisplayGenerator({
|
||||||
japaneseUtil,
|
japaneseUtil,
|
||||||
@ -938,7 +938,7 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
this._dictionaryEntries = dictionaryEntries;
|
this._dictionaryEntries = dictionaryEntries;
|
||||||
|
|
||||||
this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
|
this._updateNavigationAuto();
|
||||||
this._setNoContentVisible(dictionaryEntries.length === 0 && lookup);
|
this._setNoContentVisible(dictionaryEntries.length === 0 && lookup);
|
||||||
|
|
||||||
const container = this._container;
|
const container = this._container;
|
||||||
@ -1002,6 +1002,7 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
_clearContent() {
|
_clearContent() {
|
||||||
this._container.textContent = '';
|
this._container.textContent = '';
|
||||||
|
this._updateNavigationAuto();
|
||||||
this._setQuery('', '', 0);
|
this._setQuery('', '', 0);
|
||||||
|
|
||||||
this._triggerContentUpdateStart();
|
this._triggerContentUpdateStart();
|
||||||
@ -1058,6 +1059,10 @@ class Display extends EventDispatcher {
|
|||||||
document.title = title;
|
document.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateNavigationAuto() {
|
||||||
|
this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
|
||||||
|
}
|
||||||
|
|
||||||
_updateNavigation(previous, next) {
|
_updateNavigation(previous, next) {
|
||||||
const {documentElement} = document;
|
const {documentElement} = document;
|
||||||
if (documentElement !== null) {
|
if (documentElement !== null) {
|
||||||
|
@ -59,6 +59,8 @@ class StructuredContentGenerator {
|
|||||||
return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
|
return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
|
||||||
case 'img':
|
case 'img':
|
||||||
return this.createDefinitionImage(content, dictionary);
|
return this.createDefinitionImage(content, dictionary);
|
||||||
|
case 'a':
|
||||||
|
return this._createLinkElement(content, dictionary);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -253,4 +255,30 @@ class StructuredContentGenerator {
|
|||||||
if (typeof marginRight === 'number') { style.marginRight = `${marginRight}em`; }
|
if (typeof marginRight === 'number') { style.marginRight = `${marginRight}em`; }
|
||||||
if (typeof marginBottom === 'number') { style.marginBottom = `${marginBottom}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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,4 +69,14 @@ class AnkiTemplateRendererContentManager {
|
|||||||
}
|
}
|
||||||
this._onUnloadCallbacks = [];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,11 @@
|
|||||||
borderopacity="1.0"
|
borderopacity="1.0"
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="32"
|
inkscape:zoom="16"
|
||||||
inkscape:cx="4.203125"
|
inkscape:cx="-1.03125"
|
||||||
inkscape:cy="5.734375"
|
inkscape:cy="3.84375"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer53"
|
inkscape:current-layer="layer54"
|
||||||
showgrid="true"
|
showgrid="true"
|
||||||
units="px"
|
units="px"
|
||||||
inkscape:snap-center="true"
|
inkscape:snap-center="true"
|
||||||
@ -566,7 +566,6 @@
|
|||||||
<dc:format>image/svg+xml</dc:format>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<dc:type
|
<dc:type
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
</cc:Work>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
@ -1650,8 +1649,9 @@
|
|||||||
</g>
|
</g>
|
||||||
<g
|
<g
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer53"
|
id="g268"
|
||||||
inkscape:label="Connection">
|
inkscape:label="Connection"
|
||||||
|
style="display:none">
|
||||||
<g
|
<g
|
||||||
id="g1369"
|
id="g1369"
|
||||||
transform="matrix(0,-1,-1,0,16,16)">
|
transform="matrix(0,-1,-1,0,16,16)">
|
||||||
@ -1663,4 +1663,14 @@
|
|||||||
sodipodi:nodetypes="ssccccsssscssssccccsssscssssssssssssssssss" />
|
sodipodi:nodetypes="ssccccsssscssssccccsssscssssssssssssssssss" />
|
||||||
</g>
|
</g>
|
||||||
</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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
@ -179,6 +179,31 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
" text 3"
|
" 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"
|
100, "P E1"
|
||||||
|
Loading…
Reference in New Issue
Block a user