-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Converts rendered HTML into plain text suitable for read-aloud (TTS).
* Tables are spoken row by row, MathJax equations through their assistive
* MathML with spoken fraction/root/power structure, and mermaid diagrams as
* a list of their text labels. Other content contributes its text with
* block-level elements separated by pauses.
*/
/**
* Returns the text with all whitespace runs collapsed to single spaces.
* @param {string} text - The raw text.
* @returns {string} The normalized text.
*/
function normalize(text) {
return String(text).replace(/\s+/g, ' ');
}
/**
* Returns the visible text of a node with normalized whitespace.
* @param {Element} el - The element.
* @returns {string} The normalized text.
*/
function inline_text(el) {
return normalize(el.textContent).trim();
}
/**
* Converts the assistive MathML of a rendered equation into spoken words,
* giving fractions, roots, subscripts, and superscripts their spoken names.
* @param {Node} node - The MathML node to convert.
* @returns {string} The spoken form.
*/
function mathml_to_text(node) {
if (node.nodeType === Node.TEXT_NODE) {
return normalize(node.textContent);
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const tag = node.tagName.toLowerCase();
const children = () => [...node.childNodes].map(mathml_to_text).filter(Boolean).join(' ');
const parts = () => [...node.children].map(mathml_to_text);
switch (tag) {
case 'mfrac': {
const p = parts();
return p.length === 2 ? `fraction numerator ${p[0]} denominator ${p[1]}` : children();
}
case 'msqrt':
return `square root of ${children()}`;
case 'mroot': {
const p = parts();
return p.length === 2 ? `the ${p[1]} root of ${p[0]}` : children();
}
case 'msup': {
const p = parts();
return p.length === 2 ? `${p[0]} raised to the power ${p[1]}` : children();
}
case 'msub': {
const p = parts();
return p.length === 2 ? `${p[0]} sub ${p[1]}` : children();
}
case 'msubsup': {
const p = parts();
return p.length === 3 ? `${p[0]} sub ${p[1]} raised to the power ${p[2]}` : children();
}
default:
return children();
}
}
/**
* Extracts the spoken form of a rendered MathJax equation from its container:
* prefers MathJax's own speech label and falls back to its assistive MathML.
* @param {Element} el - The mjx-container element.
* @returns {string} The spoken equation, or an empty string when unavailable.
*/
function math_to_text(el) {
const speech = el.querySelector('mjx-speech[aria-label]');
if (speech) {
return normalize(speech.getAttribute('aria-label')).replace(/,\s*(display\s+)?math$/i, '').trim();
}
const math = el.querySelector('math');
return math ? mathml_to_text(math) : '';
}
/**
* Returns whether an svg element is a rendered mermaid diagram.
* @param {Element} svg - The svg element.
* @returns {boolean} True when it looks like a mermaid diagram.
*/
function is_mermaid_svg(svg) {
const role = svg.getAttribute('aria-roledescription') || '';
return svg.id.startsWith('mermaid') || role.toLowerCase().includes('mermaid');
}
/**
* Extracts the spoken form of a rendered mermaid diagram: its text labels in
* document order.
* @param {Element} el - The diagram container or svg element.
* @returns {string} The spoken diagram description.
*/
function diagram_to_text(el) {
const isSvg = el.tagName.toLowerCase() === 'svg';
const svg = isSvg ? el : el.querySelector('svg');
if (!svg || (isSvg && !is_mermaid_svg(svg))) return '';
const labels = [...svg.querySelectorAll('text')]
.map(text => inline_text(text))
.filter(Boolean)
.filter((label, i, all) => i === 0 || label !== all[i - 1]);
return labels.length ? `Diagram: ${labels.join(', ')}.` : 'Diagram.';
}
/**
* Converts a table into spoken text: the caption, then each row's cells
* prefixed with "Headers" for header rows and "Row" for the others.
* @param {Element} table - The table element.
* @param {string[]} out - The output chunk list.
*/
function table_to_text(table, out) {
const caption = table.querySelector(':scope > caption');
if (caption) {
out.push(`Table: ${inline_text(caption)}. `);
}
const rows = table.querySelectorAll(':scope > thead > tr, :scope > tbody > tr, :scope > tfoot > tr, :scope > tr');
rows.forEach((tr) => {
const cells = [...tr.querySelectorAll(':scope > th, :scope > td')];
if (cells.length === 0) return;
const isHeader = tr.parentElement.tagName === 'THEAD' || cells.every(cell => cell.tagName === 'TH');
const prefix = isHeader ? 'Headers: ' : 'Row: ';
out.push(`${prefix}${cells.map(cell => to_speech_text(cell)).join(', ')}. `);
});
}
/**
* Returns whether the element is a block-level element, so following content
* starts a new phrase.
* @param {Element} el - The element.
* @returns {boolean} True when block-level.
*/
function is_block(el) {
const tag = el.tagName;
return /^(P|DIV|SECTION|ARTICLE|UL|OL|LI|TABLE|THEAD|TBODY|TFOOT|TR|PRE|BLOCKQUOTE|FIGURE|FIGCAPTION|DL|DT|DD|H[1-6]|DETAILS|SUMMARY|HR)$/.test(tag);
}
/**
* Appends the spoken text of an element's children to the output list.
* @param {Element} el - The element to walk.
* @param {string[]} out - The output chunk list.
*/
function walk_element(el, out) {
const tag = el.tagName.toLowerCase();
if (tag === 'script' || tag === 'style' || tag === 'template') return;
if (tag === 'mjx-container') {
const text = math_to_text(el);
if (text) out.push(`${text} `);
return;
}
if (el.matches('.mermaid, .div-diagram-mermaid')) {
out.push(`${diagram_to_text(el)} `);
return;
}
if (tag === 'svg' && is_mermaid_svg(el)) {
out.push(`${diagram_to_text(el)} `);
return;
}
if (tag === 'table') {
table_to_text(el, out);
out.push(' ');
return;
}
if (tag === 'br' || tag === 'hr') {
out.push(' ');
return;
}
walk_children(el, out);
if (is_block(el)) {
out.push(' ');
}
}
/**
* Appends the spoken text of a node's children to the output list.
* @param {Node} node - The node to walk.
* @param {string[]} out - The output chunk list.
*/
function walk_children(node, out) {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
out.push(normalize(child.textContent));
} else if (child.nodeType === Node.ELEMENT_NODE) {
walk_element(child, out);
}
}
}
/**
* Converts rendered HTML into plain text suitable for read-aloud: tables,
* rendered equations, and diagrams are spoken in a structured form, and
* block-level boundaries become pauses.
* @param {Element|null} root - The element to convert.
* @returns {string} The speech text.
*/
export function to_speech_text(root) {
if (!root) return '';
const out = [];
walk_children(root, out);
return normalize(out.join('')).trim();
}
speech-text.js
×
Type: Web, text/plain
6.60 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/plain
6.60 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File