2020-03-09 01:31:18 +00:00
|
|
|
|
/*
|
2021-01-01 19:50:41 +00:00
|
|
|
|
* Copyright (C) 2020-2021 Yomichan Authors
|
2020-03-09 01:31:18 +00:00
|
|
|
|
*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-02-21 02:18:31 +00:00
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const assert = require('assert');
|
|
|
|
|
const {JSDOM} = require('jsdom');
|
2020-11-07 16:34:14 +00:00
|
|
|
|
const {testMain} = require('../dev/util');
|
2020-09-19 23:04:28 +00:00
|
|
|
|
const {VM} = require('../dev/vm');
|
2020-02-21 02:18:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// DOMRect class definition
|
|
|
|
|
class DOMRect {
|
|
|
|
|
constructor(x, y, width, height) {
|
|
|
|
|
this._x = x;
|
|
|
|
|
this._y = y;
|
|
|
|
|
this._width = width;
|
|
|
|
|
this._height = height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get x() { return this._x; }
|
|
|
|
|
get y() { return this._y; }
|
|
|
|
|
get width() { return this._width; }
|
|
|
|
|
get height() { return this._height; }
|
|
|
|
|
get left() { return this._x + Math.min(0, this._width); }
|
|
|
|
|
get right() { return this._x + Math.max(0, this._width); }
|
|
|
|
|
get top() { return this._y + Math.min(0, this._height); }
|
|
|
|
|
get bottom() { return this._y + Math.max(0, this._height); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createJSDOM(fileName) {
|
|
|
|
|
const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
|
|
|
|
|
const dom = new JSDOM(domSource);
|
|
|
|
|
const document = dom.window.document;
|
|
|
|
|
const window = dom.window;
|
|
|
|
|
|
|
|
|
|
// Define innerText setter as an alias for textContent setter
|
|
|
|
|
Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', {
|
|
|
|
|
set(value) { this.textContent = value; }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Placeholder for feature detection
|
|
|
|
|
document.caretRangeFromPoint = () => null;
|
|
|
|
|
|
|
|
|
|
return dom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function querySelectorChildOrSelf(element, selector) {
|
|
|
|
|
return selector ? element.querySelector(selector) : element;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getChildTextNodeOrSelf(dom, node) {
|
|
|
|
|
if (node === null) { return null; }
|
|
|
|
|
const Node = dom.window.Node;
|
|
|
|
|
const childNode = node.firstChild;
|
|
|
|
|
return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPrototypeOfOrNull(value) {
|
|
|
|
|
try {
|
|
|
|
|
return Object.getPrototypeOf(value);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findImposterElement(document) {
|
|
|
|
|
// Finds the imposter element based on it's z-index style
|
|
|
|
|
return document.querySelector('div[style*="2147483646"]>*');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function testDocument1() {
|
|
|
|
|
const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html'));
|
|
|
|
|
const window = dom.window;
|
|
|
|
|
const document = window.document;
|
|
|
|
|
const Node = window.Node;
|
|
|
|
|
const Range = window.Range;
|
|
|
|
|
|
2020-03-03 03:20:47 +00:00
|
|
|
|
const vm = new VM({document, window, Range, Node});
|
|
|
|
|
vm.execute([
|
2021-02-14 04:13:53 +00:00
|
|
|
|
'js/dom/dom-text-scanner.js',
|
|
|
|
|
'js/dom/text-source-range.js',
|
|
|
|
|
'js/dom/text-source-element.js',
|
2021-02-14 03:52:28 +00:00
|
|
|
|
'js/dom/document-util.js'
|
2020-03-03 03:20:47 +00:00
|
|
|
|
]);
|
2020-08-09 17:27:21 +00:00
|
|
|
|
const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([
|
2020-06-21 20:07:51 +00:00
|
|
|
|
'DOMTextScanner',
|
2020-03-03 03:20:47 +00:00
|
|
|
|
'TextSourceRange',
|
|
|
|
|
'TextSourceElement',
|
2020-08-09 17:27:21 +00:00
|
|
|
|
'DocumentUtil'
|
2020-03-03 03:20:47 +00:00
|
|
|
|
]);
|
2020-02-21 02:18:31 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2020-08-09 17:27:21 +00:00
|
|
|
|
await testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement});
|
2020-06-21 20:07:51 +00:00
|
|
|
|
await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner});
|
2020-02-21 02:18:31 +00:00
|
|
|
|
} finally {
|
|
|
|
|
window.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-09 17:27:21 +00:00
|
|
|
|
async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) {
|
2020-02-21 02:18:31 +00:00
|
|
|
|
const document = dom.window.document;
|
|
|
|
|
|
2020-02-22 21:32:38 +00:00
|
|
|
|
for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) {
|
2020-02-21 02:18:31 +00:00
|
|
|
|
// Get test parameters
|
|
|
|
|
let {
|
|
|
|
|
elementFromPointSelector,
|
|
|
|
|
caretRangeFromPointSelector,
|
|
|
|
|
startNodeSelector,
|
|
|
|
|
startOffset,
|
|
|
|
|
endNodeSelector,
|
|
|
|
|
endOffset,
|
|
|
|
|
resultType,
|
2021-01-10 04:10:55 +00:00
|
|
|
|
sentenceScanExtent,
|
2020-02-21 02:18:31 +00:00
|
|
|
|
sentence,
|
|
|
|
|
hasImposter
|
|
|
|
|
} = testElement.dataset;
|
|
|
|
|
|
|
|
|
|
const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector);
|
|
|
|
|
const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector);
|
|
|
|
|
const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector));
|
|
|
|
|
const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector));
|
|
|
|
|
|
|
|
|
|
startOffset = parseInt(startOffset, 10);
|
|
|
|
|
endOffset = parseInt(endOffset, 10);
|
2021-01-10 04:10:55 +00:00
|
|
|
|
sentenceScanExtent = parseInt(sentenceScanExtent, 10);
|
2020-02-21 02:18:31 +00:00
|
|
|
|
|
|
|
|
|
assert.notStrictEqual(elementFromPointValue, null);
|
|
|
|
|
assert.notStrictEqual(caretRangeFromPointValue, null);
|
|
|
|
|
assert.notStrictEqual(startNode, null);
|
|
|
|
|
assert.notStrictEqual(endNode, null);
|
|
|
|
|
|
|
|
|
|
// Setup functions
|
|
|
|
|
document.elementFromPoint = () => elementFromPointValue;
|
|
|
|
|
|
|
|
|
|
document.caretRangeFromPoint = (x, y) => {
|
|
|
|
|
const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document));
|
|
|
|
|
assert.strictEqual(!!imposter, hasImposter === 'true');
|
|
|
|
|
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
range.setStart(imposter ? imposter : startNode, startOffset);
|
|
|
|
|
range.setEnd(imposter ? imposter : startNode, endOffset);
|
|
|
|
|
|
|
|
|
|
// Override getClientRects to return a rect guaranteed to contain (x, y)
|
|
|
|
|
range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)];
|
|
|
|
|
return range;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Test docRangeFromPoint
|
2020-08-09 17:27:21 +00:00
|
|
|
|
const documentUtil = new DocumentUtil();
|
|
|
|
|
const source = documentUtil.getRangeFromPoint(0, 0, false);
|
2020-02-21 02:18:31 +00:00
|
|
|
|
switch (resultType) {
|
|
|
|
|
case 'TextSourceRange':
|
|
|
|
|
assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype);
|
|
|
|
|
break;
|
|
|
|
|
case 'TextSourceElement':
|
|
|
|
|
assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype);
|
|
|
|
|
break;
|
|
|
|
|
case 'null':
|
|
|
|
|
assert.strictEqual(source, null);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
assert.ok(false);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (source === null) { continue; }
|
|
|
|
|
|
2021-01-10 19:43:06 +00:00
|
|
|
|
// Sentence info
|
|
|
|
|
const terminatorString = '…。..??!!';
|
|
|
|
|
const terminatorMap = new Map();
|
|
|
|
|
for (const char of terminatorString) {
|
|
|
|
|
terminatorMap.set(char, [false, true]);
|
|
|
|
|
}
|
|
|
|
|
const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']];
|
|
|
|
|
const forwardQuoteMap = new Map();
|
|
|
|
|
const backwardQuoteMap = new Map();
|
|
|
|
|
for (const [char1, char2] of quoteArray) {
|
|
|
|
|
forwardQuoteMap.set(char1, [char2, false]);
|
|
|
|
|
backwardQuoteMap.set(char2, [char1, false]);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 02:18:31 +00:00
|
|
|
|
// Test docSentenceExtract
|
2021-01-10 19:43:06 +00:00
|
|
|
|
const sentenceActual = documentUtil.extractSentence(
|
|
|
|
|
source,
|
|
|
|
|
false,
|
|
|
|
|
sentenceScanExtent,
|
|
|
|
|
terminatorMap,
|
|
|
|
|
forwardQuoteMap,
|
|
|
|
|
backwardQuoteMap
|
|
|
|
|
).text;
|
2020-02-21 02:18:31 +00:00
|
|
|
|
assert.strictEqual(sentenceActual, sentence);
|
|
|
|
|
|
|
|
|
|
// Clean
|
|
|
|
|
source.cleanup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-21 20:07:51 +00:00
|
|
|
|
async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) {
|
2020-02-22 21:32:38 +00:00
|
|
|
|
const document = dom.window.document;
|
|
|
|
|
|
|
|
|
|
for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) {
|
|
|
|
|
// Get test parameters
|
|
|
|
|
let {
|
|
|
|
|
seekNodeSelector,
|
|
|
|
|
seekNodeIsText,
|
|
|
|
|
seekOffset,
|
|
|
|
|
seekLength,
|
|
|
|
|
seekDirection,
|
|
|
|
|
expectedResultNodeSelector,
|
|
|
|
|
expectedResultNodeIsText,
|
|
|
|
|
expectedResultOffset,
|
|
|
|
|
expectedResultContent
|
|
|
|
|
} = testElement.dataset;
|
|
|
|
|
|
|
|
|
|
seekOffset = parseInt(seekOffset, 10);
|
|
|
|
|
seekLength = parseInt(seekLength, 10);
|
|
|
|
|
expectedResultOffset = parseInt(expectedResultOffset, 10);
|
|
|
|
|
|
|
|
|
|
let seekNode = testElement.querySelector(seekNodeSelector);
|
|
|
|
|
if (seekNodeIsText === 'true') {
|
|
|
|
|
seekNode = seekNode.firstChild;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let expectedResultNode = testElement.querySelector(expectedResultNodeSelector);
|
|
|
|
|
if (expectedResultNodeIsText === 'true') {
|
|
|
|
|
expectedResultNode = expectedResultNode.firstChild;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {node, offset, content} = (
|
|
|
|
|
seekDirection === 'forward' ?
|
2020-06-21 20:07:51 +00:00
|
|
|
|
new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) :
|
|
|
|
|
new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength)
|
2020-02-22 21:32:38 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(node, expectedResultNode);
|
|
|
|
|
assert.strictEqual(offset, expectedResultOffset);
|
|
|
|
|
assert.strictEqual(content, expectedResultContent);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 02:18:31 +00:00
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
await testDocument1();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-11-07 16:34:14 +00:00
|
|
|
|
if (require.main === module) { testMain(main); }
|