summaryrefslogtreecommitdiff
path: root/ext/js/display/sandbox
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/display/sandbox')
-rw-r--r--ext/js/display/sandbox/pronunciation-generator.js199
-rw-r--r--ext/js/display/sandbox/structured-content-generator.js230
2 files changed, 429 insertions, 0 deletions
diff --git a/ext/js/display/sandbox/pronunciation-generator.js b/ext/js/display/sandbox/pronunciation-generator.js
new file mode 100644
index 00000000..bab36add
--- /dev/null
+++ b/ext/js/display/sandbox/pronunciation-generator.js
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+class PronunciationGenerator {
+ constructor(japaneseUtil) {
+ this._japaneseUtil = japaneseUtil;
+ }
+
+ createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions) {
+ const jp = this._japaneseUtil;
+ const nasalPositionsSet = nasalPositions.length > 0 ? new Set(nasalPositions) : null;
+ const devoicePositionsSet = devoicePositions.length > 0 ? new Set(devoicePositions) : null;
+ const container = document.createElement('span');
+ container.className = 'pronunciation-text';
+ for (let i = 0, ii = morae.length; i < ii; ++i) {
+ const i1 = i + 1;
+ const mora = morae[i];
+ const highPitch = jp.isMoraPitchHigh(i, downstepPosition);
+ const highPitchNext = jp.isMoraPitchHigh(i1, downstepPosition);
+ const nasal = nasalPositionsSet !== null && nasalPositionsSet.has(i1);
+ const devoice = devoicePositionsSet !== null && devoicePositionsSet.has(i1);
+
+ const n1 = document.createElement('span');
+ n1.className = 'pronunciation-mora';
+ n1.dataset.position = `${i}`;
+ n1.dataset.pitch = highPitch ? 'high' : 'low';
+ n1.dataset.pitchNext = highPitchNext ? 'high' : 'low';
+
+ const characterNodes = [];
+ for (const character of mora) {
+ const n2 = document.createElement('span');
+ n2.className = 'pronunciation-character';
+ n2.textContent = character;
+ n1.appendChild(n2);
+ characterNodes.push(n2);
+ }
+
+ if (devoice) {
+ n1.dataset.devoice = 'true';
+ const n3 = document.createElement('span');
+ n3.className = 'pronunciation-devoice-indicator';
+ n1.appendChild(n3);
+ }
+ if (nasal && characterNodes.length > 0) {
+ n1.dataset.nasal = 'true';
+
+ const group = document.createElement('span');
+ group.className = 'pronunciation-character-group';
+
+ const n2 = characterNodes[0];
+ const character = n2.textContent;
+
+ const characterInfo = jp.getKanaDiacriticInfo(character);
+ if (characterInfo !== null) {
+ n1.dataset.originalText = mora;
+ n2.dataset.originalText = character;
+ n2.textContent = characterInfo.character;
+ }
+
+ let n3 = document.createElement('span');
+ n3.className = 'pronunciation-nasal-diacritic';
+ n3.textContent = '\u309a'; // Combining handakuten
+ group.appendChild(n3);
+
+ n3 = document.createElement('span');
+ n3.className = 'pronunciation-nasal-indicator';
+ group.appendChild(n3);
+
+ n2.parentNode.replaceChild(group, n2);
+ group.insertBefore(n2, group.firstChild);
+ }
+
+ const line = document.createElement('span');
+ line.className = 'pronunciation-mora-line';
+ n1.appendChild(line);
+
+ container.appendChild(n1);
+ }
+ return container;
+ }
+
+ createPronunciationGraph(morae, downstepPosition) {
+ const jp = this._japaneseUtil;
+ const ii = morae.length;
+
+ const svgns = 'http://www.w3.org/2000/svg';
+ const svg = document.createElementNS(svgns, 'svg');
+ svg.setAttribute('xmlns', svgns);
+ svg.setAttribute('class', 'pronunciation-graph');
+ svg.setAttribute('focusable', 'false');
+ svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`);
+
+ if (ii <= 0) { return svg; }
+
+ const path1 = document.createElementNS(svgns, 'path');
+ svg.appendChild(path1);
+
+ const path2 = document.createElementNS(svgns, 'path');
+ svg.appendChild(path2);
+
+ const pathPoints = [];
+ for (let i = 0; i < ii; ++i) {
+ const highPitch = jp.isMoraPitchHigh(i, downstepPosition);
+ const highPitchNext = jp.isMoraPitchHigh(i + 1, downstepPosition);
+ const x = i * 50 + 25;
+ const y = highPitch ? 25 : 75;
+ if (highPitch && !highPitchNext) {
+ this._addGraphDotDownstep(svg, svgns, x, y);
+ } else {
+ this._addGraphDot(svg, svgns, x, y);
+ }
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ path1.setAttribute('class', 'pronunciation-graph-line');
+ path1.setAttribute('d', `M${pathPoints.join(' L')}`);
+
+ pathPoints.splice(0, ii - 1);
+ {
+ const highPitch = jp.isMoraPitchHigh(ii, downstepPosition);
+ const x = ii * 50 + 25;
+ const y = highPitch ? 25 : 75;
+ this._addGraphTriangle(svg, svgns, x, y);
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ path2.setAttribute('class', 'pronunciation-graph-line-tail');
+ path2.setAttribute('d', `M${pathPoints.join(' L')}`);
+
+ return svg;
+ }
+
+ createPronunciationDownstepNotation(downstepPosition) {
+ downstepPosition = `${downstepPosition}`;
+
+ const n1 = document.createElement('span');
+ n1.className = 'pronunciation-downstep-notation';
+ n1.dataset.downstepPosition = downstepPosition;
+
+ let n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-prefix';
+ n2.textContent = '[';
+ n1.appendChild(n2);
+
+ n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-number';
+ n2.textContent = downstepPosition;
+ n1.appendChild(n2);
+
+ n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-suffix';
+ n2.textContent = ']';
+ n1.appendChild(n2);
+
+ return n1;
+ }
+
+ // Private
+
+ _addGraphDot(container, svgns, x, y) {
+ container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot', x, y, '15'));
+ }
+
+ _addGraphDotDownstep(container, svgns, x, y) {
+ container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot-downstep1', x, y, '15'));
+ container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot-downstep2', x, y, '5'));
+ }
+
+ _addGraphTriangle(container, svgns, x, y) {
+ const node = document.createElementNS(svgns, 'path');
+ node.setAttribute('class', 'pronunciation-graph-triangle');
+ node.setAttribute('d', 'M0 13 L15 -13 L-15 -13 Z');
+ node.setAttribute('transform', `translate(${x},${y})`);
+ container.appendChild(node);
+ }
+
+ _createGraphCircle(svgns, className, x, y, radius) {
+ const node = document.createElementNS(svgns, 'circle');
+ node.setAttribute('class', className);
+ node.setAttribute('cx', `${x}`);
+ node.setAttribute('cy', `${y}`);
+ node.setAttribute('r', radius);
+ return node;
+ }
+}
diff --git a/ext/js/display/sandbox/structured-content-generator.js b/ext/js/display/sandbox/structured-content-generator.js
new file mode 100644
index 00000000..833df6f6
--- /dev/null
+++ b/ext/js/display/sandbox/structured-content-generator.js
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+class StructuredContentGenerator {
+ constructor(mediaLoader, document) {
+ this._mediaLoader = mediaLoader;
+ this._document = document;
+ }
+
+ createStructuredContent(content, dictionary) {
+ if (typeof content === 'string') {
+ return this._createTextNode(content);
+ }
+ if (!(typeof content === 'object' && content !== null)) {
+ return null;
+ }
+ if (Array.isArray(content)) {
+ const fragment = this._createDocumentFragment();
+ for (const item of content) {
+ const child = this.createStructuredContent(item, dictionary);
+ if (child !== null) { fragment.appendChild(child); }
+ }
+ return fragment;
+ }
+ const {tag} = content;
+ switch (tag) {
+ case 'br':
+ return this._createStructuredContentElement(tag, content, dictionary, 'simple', false, false);
+ case 'ruby':
+ case 'rt':
+ case 'rp':
+ return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, false);
+ case 'table':
+ return this._createStructuredContentTableElement(tag, content, dictionary);
+ case 'thead':
+ case 'tbody':
+ case 'tfoot':
+ case 'tr':
+ return this._createStructuredContentElement(tag, content, dictionary, 'table', true, false);
+ case 'th':
+ case 'td':
+ return this._createStructuredContentElement(tag, content, dictionary, 'table-cell', true, true);
+ case 'div':
+ case 'span':
+ return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
+ case 'img':
+ return this.createDefinitionImage(content, dictionary);
+ }
+ return null;
+ }
+
+ createDefinitionImage(data, dictionary) {
+ const {
+ path,
+ width,
+ height,
+ preferredWidth,
+ preferredHeight,
+ title,
+ pixelated,
+ imageRendering,
+ appearance,
+ background,
+ collapsed,
+ collapsible,
+ verticalAlign,
+ sizeUnits
+ } = data;
+
+ const hasPreferredWidth = (typeof preferredWidth === 'number');
+ const hasPreferredHeight = (typeof preferredHeight === 'number');
+ const invAspectRatio = (
+ hasPreferredWidth && hasPreferredHeight ?
+ preferredHeight / preferredWidth :
+ height / width
+ );
+ const usedWidth = (
+ hasPreferredWidth ?
+ preferredWidth :
+ (hasPreferredHeight ? preferredHeight * invAspectRatio : width)
+ );
+
+ const node = this._createElement('a', 'gloss-image-link');
+ node.target = '_blank';
+ node.rel = 'noreferrer noopener';
+
+ const imageContainer = this._createElement('span', 'gloss-image-container');
+ node.appendChild(imageContainer);
+
+ const aspectRatioSizer = this._createElement('span', 'gloss-image-sizer');
+ imageContainer.appendChild(aspectRatioSizer);
+
+ const imageBackground = this._createElement('span', 'gloss-image-background');
+ imageContainer.appendChild(imageBackground);
+
+ const image = this._createElement('img', 'gloss-image');
+ image.alt = '';
+ imageContainer.appendChild(image);
+
+ const overlay = this._createElement('span', 'gloss-image-container-overlay');
+ imageContainer.appendChild(overlay);
+
+ const linkText = this._createElement('span', 'gloss-image-link-text');
+ linkText.textContent = 'Image';
+ node.appendChild(linkText);
+
+ node.dataset.path = path;
+ node.dataset.dictionary = dictionary;
+ node.dataset.imageLoadState = 'not-loaded';
+ node.dataset.hasAspectRatio = 'true';
+ node.dataset.imageRendering = typeof imageRendering === 'string' ? imageRendering : (pixelated ? 'pixelated' : 'auto');
+ node.dataset.appearance = typeof appearance === 'string' ? appearance : 'auto';
+ node.dataset.background = typeof background === 'boolean' ? `${background}` : 'true';
+ node.dataset.collapsed = typeof collapsed === 'boolean' ? `${collapsed}` : 'false';
+ node.dataset.collapsible = typeof collapsible === 'boolean' ? `${collapsible}` : 'true';
+ if (typeof verticalAlign === 'string') {
+ node.dataset.verticalAlign = verticalAlign;
+ }
+ if (typeof sizeUnits === 'string' && (hasPreferredWidth || hasPreferredHeight)) {
+ node.dataset.sizeUnits = sizeUnits;
+ }
+
+ imageContainer.style.width = `${usedWidth}em`;
+ if (typeof title === 'string') {
+ imageContainer.title = title;
+ }
+
+ aspectRatioSizer.style.paddingTop = `${invAspectRatio * 100.0}%`;
+
+ if (this._mediaLoader !== null) {
+ this._mediaLoader.loadMedia(
+ path,
+ dictionary,
+ (url) => this._setImageData(node, image, imageBackground, url, false),
+ () => this._setImageData(node, image, imageBackground, null, true)
+ );
+ }
+
+ return node;
+ }
+
+ // Private
+
+ _createElement(tagName, className) {
+ const node = this._document.createElement(tagName);
+ node.className = className;
+ return node;
+ }
+
+ _createTextNode(data) {
+ return this._document.createTextNode(data);
+ }
+
+ _createDocumentFragment() {
+ return this._document.createDocumentFragment();
+ }
+
+ _setImageData(node, image, imageBackground, url, unloaded) {
+ if (url !== null) {
+ image.src = url;
+ node.href = url;
+ node.dataset.imageLoadState = 'loaded';
+ imageBackground.style.setProperty('--image', `url("${url}")`);
+ } else {
+ image.removeAttribute('src');
+ node.removeAttribute('href');
+ node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error';
+ imageBackground.style.removeProperty('--image');
+ }
+ }
+
+ _createStructuredContentTableElement(tag, content, dictionary) {
+ const container = this._createElement('div', 'gloss-sc-table-container');
+ const table = this._createStructuredContentElement(tag, content, dictionary, 'table', true, false);
+ container.appendChild(table);
+ return container;
+ }
+
+ _createStructuredContentElement(tag, content, dictionary, type, hasChildren, hasStyle) {
+ const node = this._createElement(tag, `gloss-sc-${tag}`);
+ switch (type) {
+ case 'table-cell':
+ {
+ const {colSpan, rowSpan} = content;
+ if (typeof colSpan === 'number') { node.colSpan = colSpan; }
+ if (typeof rowSpan === 'number') { node.rowSpan = rowSpan; }
+ }
+ break;
+ }
+ if (hasStyle) {
+ const {style} = content;
+ if (typeof style === 'object' && style !== null) {
+ this._setStructuredContentElementStyle(node, style);
+ }
+ }
+ if (hasChildren) {
+ const child = this.createStructuredContent(content.content, dictionary);
+ if (child !== null) { node.appendChild(child); }
+ }
+ return node;
+ }
+
+ _setStructuredContentElementStyle(node, contentStyle) {
+ const {style} = node;
+ const {fontStyle, fontWeight, fontSize, textDecorationLine, verticalAlign} = contentStyle;
+ if (typeof fontStyle === 'string') { style.fontStyle = fontStyle; }
+ if (typeof fontWeight === 'string') { style.fontWeight = fontWeight; }
+ if (typeof fontSize === 'string') { style.fontSize = fontSize; }
+ if (typeof verticalAlign === 'string') { style.verticalAlign = verticalAlign; }
+ if (typeof textDecorationLine === 'string') {
+ style.textDecoration = textDecorationLine;
+ } else if (Array.isArray(textDecorationLine)) {
+ style.textDecoration = textDecorationLine.join(' ');
+ }
+ }
+}