mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
263 lines
8.3 KiB
JavaScript
263 lines
8.3 KiB
JavaScript
/*
|
|
* Copyright (C) 2023-2025 Yomitan Authors
|
|
* Copyright (C) 2016-2022 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/>.
|
|
*/
|
|
|
|
import {readCodePointsBackward, readCodePointsForward} from '../data/string-util.js';
|
|
import {convertMultipleRectZoomCoordinates} from './document-util.js';
|
|
|
|
/**
|
|
* This class represents a text source that is attached to a HTML element, such as an <img>
|
|
* with alt text or a <button>.
|
|
*/
|
|
export class TextSourceElement {
|
|
/**
|
|
* Creates a new instance of the class.
|
|
* @param {Element} element The source element.
|
|
* @param {string} fullContent The string representing the element's full text value.
|
|
* @param {number} startOffset The text start offset position within the full content.
|
|
* @param {number} endOffset The text end offset position within the full content.
|
|
*/
|
|
constructor(element, fullContent, startOffset, endOffset) {
|
|
/** @type {Element} */
|
|
this._element = element;
|
|
/** @type {string} */
|
|
this._fullContent = fullContent;
|
|
/** @type {number} */
|
|
this._startOffset = startOffset;
|
|
/** @type {number} */
|
|
this._endOffset = endOffset;
|
|
/** @type {string} */
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
|
}
|
|
|
|
/**
|
|
* Gets the type name of this instance.
|
|
* @type {'element'}
|
|
*/
|
|
get type() {
|
|
return 'element';
|
|
}
|
|
|
|
/**
|
|
* The source element.
|
|
* @type {Element}
|
|
*/
|
|
get element() {
|
|
return this._element;
|
|
}
|
|
|
|
/**
|
|
* The string representing the element's full text value.
|
|
* @type {string}
|
|
*/
|
|
get fullContent() {
|
|
return this._fullContent;
|
|
}
|
|
|
|
/**
|
|
* The string representing the element's constrained text value.
|
|
* @type {string}
|
|
*/
|
|
get content() {
|
|
return this._content;
|
|
}
|
|
|
|
/**
|
|
* The text start offset position within the full content.
|
|
* @type {number}
|
|
*/
|
|
get startOffset() {
|
|
return this._startOffset;
|
|
}
|
|
|
|
/**
|
|
* The text end offset position within the full content.
|
|
* @type {number}
|
|
*/
|
|
get endOffset() {
|
|
return this._endOffset;
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of the instance.
|
|
* @returns {TextSourceElement} The new clone.
|
|
*/
|
|
clone() {
|
|
return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset);
|
|
}
|
|
|
|
/**
|
|
* Performs any cleanup that is necessary after the element has been used.
|
|
*/
|
|
cleanup() {
|
|
// NOP
|
|
}
|
|
|
|
/**
|
|
* Gets the selected text of element, which is a substring of the full content
|
|
* starting at `startOffset` and ending at `endOffset`.
|
|
* @returns {string} The text content.
|
|
*/
|
|
text() {
|
|
return this._content;
|
|
}
|
|
|
|
/**
|
|
* Moves the end offset of the text by a set amount of unicode codepoints.
|
|
* @param {number} length The maximum number of codepoints to move by.
|
|
* @param {boolean} fromEnd Whether to move the offset from the current end position (if `true`) or the start position (if `false`).
|
|
* @returns {number} The actual number of characters (not codepoints) that were read.
|
|
*/
|
|
setEndOffset(length, fromEnd) {
|
|
const offset = fromEnd ? this._endOffset : this._startOffset;
|
|
length = Math.min(this._fullContent.length - offset, length);
|
|
if (length > 0) {
|
|
length = readCodePointsForward(this._fullContent, offset, length).length;
|
|
}
|
|
this._endOffset = offset + length;
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Moves the start offset of the text by a set amount of unicode codepoints.
|
|
* @param {number} length The maximum number of codepoints to move by.
|
|
* @returns {number} The actual number of characters (not codepoints) that were read.
|
|
*/
|
|
setStartOffset(length) {
|
|
length = Math.min(this._startOffset, length);
|
|
if (length > 0) {
|
|
length = readCodePointsBackward(this._fullContent, this._startOffset - 1, length).length;
|
|
}
|
|
this._startOffset -= length;
|
|
this._content = this._fullContent.substring(this._startOffset, this._endOffset);
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Gets the rects that represent the position and bounds of the text source.
|
|
* @returns {DOMRect[]} The rects.
|
|
*/
|
|
getRects() {
|
|
return convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element);
|
|
}
|
|
|
|
/**
|
|
* Gets writing mode that is used for this element.
|
|
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode.
|
|
* @returns {import('document-util').NormalizedWritingMode} The writing mode.
|
|
*/
|
|
getWritingMode() {
|
|
return 'horizontal-tb';
|
|
}
|
|
|
|
/**
|
|
* Selects the text source in the document.
|
|
*/
|
|
select() {
|
|
// NOP
|
|
}
|
|
|
|
/**
|
|
* Deselects the text source in the document.
|
|
*/
|
|
deselect() {
|
|
// NOP
|
|
}
|
|
|
|
/**
|
|
* Checks whether another text source has the same starting point.
|
|
* @param {import('text-source').TextSource} other The other source to test.
|
|
* @returns {boolean} `true` if the starting points are equivalent, `false` otherwise.
|
|
*/
|
|
hasSameStart(other) {
|
|
return (
|
|
typeof other === 'object' &&
|
|
other !== null &&
|
|
other instanceof TextSourceElement &&
|
|
this._element === other.element &&
|
|
this._fullContent === other.fullContent &&
|
|
this._startOffset === other.startOffset
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gets a list of the nodes in this text source's range.
|
|
* @returns {Node[]} The nodes in the range.
|
|
*/
|
|
getNodesInRange() {
|
|
return [this._element];
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance for a given element.
|
|
* @param {Element} element The source element.
|
|
* @returns {TextSourceElement} A new instance of the class corresponding to the element.
|
|
*/
|
|
static create(element) {
|
|
return new TextSourceElement(element, this._getElementContent(element), 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Gets the full content string for a given element.
|
|
* @param {Element} element The element to get the full content of.
|
|
* @returns {string} The content string.
|
|
*/
|
|
static _getElementContent(element) {
|
|
let content = '';
|
|
switch (element.nodeName.toUpperCase()) {
|
|
case 'BUTTON':
|
|
{
|
|
const {textContent} = /** @type {HTMLButtonElement} */ (element);
|
|
if (textContent !== null) {
|
|
content = textContent;
|
|
}
|
|
}
|
|
break;
|
|
case 'IMG':
|
|
{
|
|
const alt = /** @type {HTMLImageElement} */ (element).getAttribute('alt');
|
|
if (typeof alt === 'string') {
|
|
content = alt;
|
|
}
|
|
}
|
|
break;
|
|
case 'SELECT':
|
|
{
|
|
const {selectedIndex, options} = /** @type {HTMLSelectElement} */ (element);
|
|
if (selectedIndex >= 0 && selectedIndex < options.length) {
|
|
const {textContent} = options[selectedIndex];
|
|
if (textContent !== null) {
|
|
content = textContent;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'INPUT':
|
|
{
|
|
content = /** @type {HTMLInputElement} */ (element).value;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Remove zero-width space, zero-width non-joiner, soft hyphen
|
|
content = content.replace(/[\u200b\u200c\u00ad]/g, '');
|
|
|
|
return content;
|
|
}
|
|
}
|