From d4ae9aa501ece99ea6c5e6b8fb01c3005f5b7f03 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 13:05:43 -0400 Subject: DOMTextScanner (#458) * Create new class for scanning text in a document * Update test styles * Add tests --- test/data/html/test-dom-text-scanner.html | 393 ++++++++++++++++++++++++++++++ test/data/html/test-stylesheet.css | 12 +- test/test-dom-text-scanner.js | 181 ++++++++++++++ 3 files changed, 583 insertions(+), 3 deletions(-) create mode 100644 test/data/html/test-dom-text-scanner.html create mode 100644 test/test-dom-text-scanner.js (limited to 'test') diff --git a/test/data/html/test-dom-text-scanner.html b/test/data/html/test-dom-text-scanner.html new file mode 100644 index 00000000..6b78570a --- /dev/null +++ b/test/data/html/test-dom-text-scanner.html @@ -0,0 +1,393 @@ + + + + + + Yomichan DOMTextScanner Tests + + + + + +

Yomichan DOMTextScanner Tests

+ + + Layout newlines expected due to entering and exiting display:block nodes. +
小ぢん
まり1
+
小ぢん
まり2
+
+ + + Layout newline expected due to sequential display:block elements. +
小ぢんまり1
小ぢんまり2
+
+ + + Layout newline expected due to sequential display:block elements separated by a newline. +
小ぢんまり1
+
小ぢんまり2
+
+ + + No newlines expected due to display:inline. +小ぢんまり1小ぢんまり2 + + + + No newlines expected due to white-space:normal. +小ぢんまり1 +小ぢんまり2 + + + + Newline expected due to white-space:pre. +
+小ぢんまり1
+小ぢんまり2
+
+
+ + + No newlines expected due to display:inline-block. Actual layout flow cannot be determined by DOM/CSS alone. +小ぢんまり1小ぢんまり2 + + + + Single newline expected due to display:block layout. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:absolute causing a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:fixed causing a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:sticky being able to cause a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Scanning text starting in an <rt> element. Should start scanning at the start of the <ruby> tag instead. +
()ぢんまり1
+
+ + + Skip <script> content. +
小ぢんまり1
+
+ + + Skip <style> content. +
小ぢんまり1
+
+ + + Skip <textarea> content. +
小ぢんまり1
+
+ + + Skip <input> content. +
小ぢんまり1
+
+ + + Skip <button> content. +
小ぢんまり1
+
+ + + Skip content with font-size:0. +
小ぢんcontentまり1
+
+ + + Skip content with opacity:0. +
小ぢんcontentまり1
+
+ + + Skip content with visibility:hidden. +
小ぢんcontentまり1
+
+ + + Skip content with display:none. +
小ぢんcontentまり1
+
+ + + Don't skip content with user-select:none. +
小ぢまり1
+
+ + + Skip content with user-select:none and a transparent color. +
小ぢんcontentまり1
+
+ + + \ No newline at end of file diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css index f63d2481..2e9a2f52 100644 --- a/test/data/html/test-stylesheet.css +++ b/test/data/html/test-stylesheet.css @@ -28,7 +28,9 @@ a, a:visited { text-decoration: underline; } -.test { +.test, +y-test { + display: block; background-color: #ffffff; margin: 1em 0; padding: 0.5em; @@ -36,7 +38,8 @@ a, a:visited { border-radius: 4px; } -.test:before { +.test:before, +y-test:before { content: "Test " counter(test-id); display: block; counter-increment: test-id; @@ -45,7 +48,10 @@ a, a:visited { font-weight: bold; } -.description { +.description, +y-description { color: #444444; font-style: italic; + display: block; + padding-bottom: 0.5em; } diff --git a/test/test-dom-text-scanner.js b/test/test-dom-text-scanner.js new file mode 100644 index 00000000..41d6e307 --- /dev/null +++ b/test/test-dom-text-scanner.js @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 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 . + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {JSDOM} = require('jsdom'); +const {VM} = require('./yomichan-vm'); + + +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + return new JSDOM(domSource); +} + +function querySelectorTextNode(element, selector) { + let textIndex = -1; + const match = /::text$|::nth-text\((\d+)\)$/.exec(selector); + if (match !== null) { + textIndex = (match[1] ? parseInt(match[1], 10) - 1 : 0); + selector = selector.substring(0, selector.length - match[0].length); + } + const result = element.querySelector(selector); + if (textIndex < 0) { + return result; + } + for (let n = result.firstChild; n !== null; n = n.nextSibling) { + if (n.nodeType === n.constructor.TEXT_NODE) { + if (textIndex === 0) { + return n; + } + --textIndex; + } + } + return null; +} + + +function getComputedFontSizeInPixels(window, getComputedStyle, element) { + for (; element !== null; element = element.parentNode) { + if (element.nodeType === window.Node.ELEMENT_NODE) { + const fontSize = getComputedStyle(element).fontSize; + if (fontSize.endsWith('px')) { + const value = parseFloat(fontSize.substring(0, fontSize.length - 2)); + return value; + } + } + } + const defaultFontSize = 14; + return defaultFontSize; +} + +function createAbsoluteGetComputedStyle(window) { + // Wrapper to convert em units to px units + const getComputedStyleOld = window.getComputedStyle.bind(window); + return (element, ...args) => { + const style = getComputedStyleOld(element, ...args); + return new Proxy(style, { + get: (target, property) => { + let result = target[property]; + if (typeof result === 'string') { + result = result.replace(/([-+]?\d(?:\.\d)?(?:[eE][-+]?\d+)?)em/g, (g0, g1) => { + const fontSize = getComputedFontSizeInPixels(window, getComputedStyleOld, element); + return `${parseFloat(g1) * fontSize}px`; + }); + } + return result; + } + }); + }; +} + + +async function testDomTextScanner(dom, {DOMTextScanner}) { + const document = dom.window.document; + for (const testElement of document.querySelectorAll('y-test')) { + let testData = JSON.parse(testElement.dataset.testData); + if (!Array.isArray(testData)) { + testData = [testData]; + } + for (const testDataItem of testData) { + let { + node, + offset, + length, + forcePreserveWhitespace, + generateLayoutContent, + reversible, + expected: { + node: expectedNode, + offset: expectedOffset, + content: expectedContent + } + } = testDataItem; + + node = querySelectorTextNode(testElement, node); + expectedNode = querySelectorTextNode(testElement, expectedNode); + + // Standard test + { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length); + + const {node: actualNode1, offset: actualOffset1, content: actualContent1} = scanner; + assert.strictEqual(actualContent1, expectedContent); + assert.strictEqual(actualOffset1, expectedOffset); + assert.strictEqual(actualNode1, expectedNode); + } + + // Substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length - i); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(0, expectedContent.length - i)); + } + + if (reversible === false) { continue; } + + // Reversed test + { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-length); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent); + } + + // Reversed substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-(length - i)); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(i)); + } + } + } +} + + +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html')); + const window = dom.window; + try { + const {document, Node, Range} = window; + + window.getComputedStyle = createAbsoluteGetComputedStyle(window); + + const vm = new VM({document, window, Range, Node}); + vm.execute('fg/js/dom-text-scanner.js'); + const DOMTextScanner = vm.get('DOMTextScanner'); + + await testDomTextScanner(dom, {DOMTextScanner}); + } finally { + window.close(); + } +} + + +async function main() { + await testDocument1(); +} + + +if (require.main === module) { main(); } -- cgit v1.2.3