From bc47909746d432b1b30e4b0d477cb5cbaf73c509 Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Fri, 9 Oct 2026 20:42:21 -0700 Subject: [PATCH 1/6] Fix rubyReserve regression --- src/main/js/html.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/js/html.js b/src/main/js/html.js index 5385fbb2..399c3e76 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -900,14 +900,14 @@ function applyRubyReserve(lineList, context) { if (context.rubyReserve[0] === "both" || (context.rubyReserve[0] === "outside" && lineList.length == 1)) { - rt1 = document.createElement("span"); // rtc element is deprecated in HTML - rt1.style.display = "ruby-text-container"; + rt1 = document.createElement("rt"); + rt1.style.display = "ruby-text-container"; // for Firefox rt1.style[RUBYPOSITION_PROP] = RUBYPOSITION_ISWK ? "after" : "under"; rt1.textContent = "\u200B"; rt1.style.fontSize = fs; - rt2 = document.createElement("span"); // rtc element is deprecated in HTML - rt2.style.display = "ruby-text-container"; + rt2 = document.createElement("rt"); + rt2.style.display = "ruby-text-container"; // for Firefox rt2.style[RUBYPOSITION_PROP] = RUBYPOSITION_ISWK ? "before" : "over"; rt2.textContent = "\u200B"; rt2.style.fontSize = fs; @@ -917,8 +917,8 @@ function applyRubyReserve(lineList, context) { } else { - rt1 = document.createElement("span"); // rtc element is deprecated in HTML - rt1.style.display = "ruby-text-container"; + rt1 = document.createElement("rt"); + rt1.style.display = "ruby-text-container"; // for Firefox rt1.textContent = "\u200B"; rt1.style.fontSize = fs; From 2d936e6401d1afd383f92a6af1032d7dd60f8c4b Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Fri, 9 Oct 2026 22:49:55 -0700 Subject: [PATCH 2/6] Making progress --- src/main/js/html.js | 196 ++++++++++++++++++++++++++++++-------------- 1 file changed, 134 insertions(+), 62 deletions(-) diff --git a/src/main/js/html.js b/src/main/js/html.js index 399c3e76..3c903def 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -211,15 +211,6 @@ function processElement(context, dom_parent, isd_element, isd_parent) { e = document.createElement("rt"); - } else if (isd_element.styleAttrs[byName.ruby.qname] === "baseContainer") { - - e = document.createElement("span"); // rbc element is deprecated in HTML - - } else if (isd_element.styleAttrs[byName.ruby.qname] === "textContainer") { - - e = document.createElement("span"); // rtc element is deprecated in HTML - e.style.display = "ruby-text-container"; - } else if (isd_element.styleAttrs[byName.ruby.qname] === "delimiter") { /* ignore rp */ @@ -455,9 +446,19 @@ function processElement(context, dom_parent, isd_element, isd_parent) { if ("contents" in isd_element) { - for (let k = 0; k < isd_element.contents.length; k++) { + if (isd_element.kind === "span" && + isd_element.styleAttrs[byName.ruby.qname] === "container" && + isd_element.contents.some((c) => c.styleAttrs[byName.ruby.qname] === "baseContainer")) { + + processRubyContainer(context, e, isd_element); - processElement(context, proc_e, isd_element.contents[k], isd_element); + } else { + + for (let k = 0; k < isd_element.contents.length; k++) { + + processElement(context, proc_e, isd_element.contents[k], isd_element); + + } } @@ -583,6 +584,81 @@ function processElement(context, dom_parent, isd_element, isd_parent) { } } +/* + * Maps a TTML ruby container that contains a ruby base container to N + * elements, one for each ruby base of the base container, since and + * are deprecated. + * + * The ith element contains: + * - the ith ruby base, or, if there are two ruby text containers, a nested + * element that contains the ith ruby base and the ith ruby text of the + * second text container; followed by + * - the ith ruby text of the first text container. + */ +function processRubyContainer(context, ruby, isd_ruby) { + + let bases = []; + + const textContainers = []; + + for (const c of isd_ruby.contents) { + + if (c.styleAttrs[byName.ruby.qname] === "baseContainer") { + + bases = c.contents.filter((b) => b.styleAttrs[byName.ruby.qname] === "base"); + + } else if (c.styleAttrs[byName.ruby.qname] === "textContainer") { + + textContainers.push(c.contents.filter((t) => t.styleAttrs[byName.ruby.qname] === "text")); + + } + + } + + const template = ruby.cloneNode(false); + + let outer = ruby; + + for (let i = 0; i < bases.length; i++) { + + if (i > 0) { + + const next = template.cloneNode(false); + + outer.parentElement.insertBefore(next, outer.nextSibling); + + outer = next; + + } + + let base_parent = outer; + + if (textContainers.length > 1) { + + base_parent = template.cloneNode(false); + + outer.appendChild(base_parent); + + } + + processElement(context, base_parent, bases[i], isd_ruby); + + if (textContainers.length > 1 && i < textContainers[1].length) { + + processElement(context, base_parent, textContainers[1][i], isd_ruby); + + } + + if (textContainers.length > 0 && i < textContainers[0].length) { + + processElement(context, outer, textContainers[0][i], isd_ruby); + + } + + } + +} + function mergeSpans(lineList, context) { for (let i = 0; i < lineList.length; i++) { @@ -886,88 +962,84 @@ function applyRubyReserve(lineList, context) { for (let i = 0; i < lineList.length; i++) { - const ruby = document.createElement("ruby"); - - const rb = document.createElement("span"); // rb element is deprecated in HTML - rb.textContent = "\u200B"; - - ruby.appendChild(rb); + const fs = context.rubyReserve[1].toUsedLength(context.w, context.h) + "px"; - let rt1; - let rt2; + /* We cannot use the deprecated to reserve space, so instead we use nested rubies */ - const fs = context.rubyReserve[1].toUsedLength(context.w, context.h) + "px"; + const reserved_pos = []; if (context.rubyReserve[0] === "both" || (context.rubyReserve[0] === "outside" && lineList.length == 1)) { - rt1 = document.createElement("rt"); - rt1.style.display = "ruby-text-container"; // for Firefox - rt1.style[RUBYPOSITION_PROP] = RUBYPOSITION_ISWK ? "after" : "under"; - rt1.textContent = "\u200B"; - rt1.style.fontSize = fs; + reserved_pos.push(RUBYPOSITION_ISWK ? "after" : "under"); + reserved_pos.push(RUBYPOSITION_ISWK ? "before" : "over"); - rt2 = document.createElement("rt"); - rt2.style.display = "ruby-text-container"; // for Firefox - rt2.style[RUBYPOSITION_PROP] = RUBYPOSITION_ISWK ? "before" : "over"; - rt2.textContent = "\u200B"; - rt2.style.fontSize = fs; + } else if (context.rubyReserve[0] === "after" || (context.rubyReserve[0] === "outside" && i > 0)) { - ruby.appendChild(rt1); - ruby.appendChild(rt2); + reserved_pos.push(RUBYPOSITION_ISWK ? "after" : ((context.bpd === "tb" || context.bpd === "rl") ? "under" : "over")); } else { - rt1 = document.createElement("rt"); - rt1.style.display = "ruby-text-container"; // for Firefox - rt1.textContent = "\u200B"; - rt1.style.fontSize = fs; + reserved_pos.push(RUBYPOSITION_ISWK ? "before" : ((context.bpd === "tb" || context.bpd === "rl") ? "over" : "under")); - let pos; + } - if (context.rubyReserve[0] === "after" || (context.rubyReserve[0] === "outside" && i > 0)) { + /* add in front of the first ruby element of the line, if it exists */ - pos = RUBYPOSITION_ISWK ? "after" : ((context.bpd === "tb" || context.bpd === "rl") ? "under" : "over"); + let sib = null; - } else { + for (let j = 0; j < lineList[i].rbc.length; j++) { - pos = RUBYPOSITION_ISWK ? "before" : ((context.bpd === "tb" || context.bpd === "rl") ? "over" : "under"); + if (lineList[i].rbc[j].localName === "ruby") { + sib = lineList[i].rbc[j]; + + break; } - rt1.style[RUBYPOSITION_PROP] = pos; + } - ruby.appendChild(rt1); + const copy_styles_from = sib; - } + /* otherwise add before first span */ - /* add in front of the first ruby element of the line, if it exists */ + sib = sib || lineList[i].elements[0].node; - let sib = null; + /* each annotation is placed in its own ruby element since Chrome ignores ruby-position on rt elements + and positions all the annotations of a ruby element according to its ruby-position */ - for (let j = 0; j < lineList[i].rbc.length; j++) { + for (const pos of reserved_pos) { - if (lineList[i].rbc[j].localName === "ruby") { + const ruby = document.createElement("ruby"); - sib = lineList[i].rbc[j]; + const rb = document.createElement("span"); // rb element is deprecated in HTML + rb.textContent = "\u200B"; - /* copy specified style properties from the sibling ruby container */ + ruby.appendChild(rb); - for (let k = 0; k < sib.style.length; k++) { + const rt = document.createElement("rt"); + rt.style[RUBYPOSITION_PROP] = pos; + rt.textContent = "\u200B"; + rt.style.fontSize = fs; - ruby.style.setProperty(sib.style.item(k), sib.style.getPropertyValue(sib.style.item(k))); + ruby.appendChild(rt); + + /* copy specified style properties from the sibling ruby container */ + + if (copy_styles_from !== null) { + + for (let k = 0; k < copy_styles_from.style.length; k++) { + + ruby.style.setProperty(copy_styles_from.style.item(k), copy_styles_from.style.getPropertyValue(copy_styles_from.style.item(k))); } - break; } - } + ruby.style[RUBYPOSITION_PROP] = pos; // for Chrome - /* otherwise add before first span */ + sib.parentElement.insertBefore(ruby, sib); - sib = sib || lineList[i].elements[0].node; - - sib.parentElement.insertBefore(ruby, sib); + } } @@ -1113,7 +1185,7 @@ function rect2edges(rect, context) { function constructLineList(context, element, llist, bgcolor) { - if (element.localName === "rt" || element.style.display === "ruby-text-container") { + if (element.localName === "rt") { /* skip ruby annotations */ @@ -1205,9 +1277,9 @@ function constructLineList(context, element, llist, bgcolor) { constructLineList(context, child, llist, curbgcolor); - if (child.localName === "ruby" || child.style.display === "ruby-text-container") { + if (child.localName === "ruby") { - /* remember non-empty ruby and rtc elements so that tts:rubyPosition can be applied */ + /* remember non-empty ruby elements so that tts:rubyPosition can be applied */ if (llist.length > 0) { @@ -1640,7 +1712,7 @@ const STYLING_MAP_DEFS = [ } - /* apply position to the parent dom_element, i.e. ruby or rtc */ + /* apply position to the parent dom_element, i.e. ruby */ dom_element.parentElement.style[RUBYPOSITION_PROP] = pos; } From d1988f1dfb159d86efaebaf7a9a92db44d301449 Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Fri, 9 Oct 2026 23:01:43 -0700 Subject: [PATCH 3/6] Use nested rubies for rubyReserve --- src/main/js/html.js | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/main/js/html.js b/src/main/js/html.js index 3c903def..d390e56a 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -985,36 +985,27 @@ function applyRubyReserve(lineList, context) { /* add in front of the first ruby element of the line, if it exists */ - let sib = null; + let ruby_sibling = null; for (let j = 0; j < lineList[i].rbc.length; j++) { if (lineList[i].rbc[j].localName === "ruby") { - sib = lineList[i].rbc[j]; + ruby_sibling = lineList[i].rbc[j]; break; } } - const copy_styles_from = sib; - - /* otherwise add before first span */ - - sib = sib || lineList[i].elements[0].node; - - /* each annotation is placed in its own ruby element since Chrome ignores ruby-position on rt elements - and positions all the annotations of a ruby element according to its ruby-position */ + let base = document.createElement("span"); // rb element is deprecated in HTML + base.textContent = "\u200B"; for (const pos of reserved_pos) { const ruby = document.createElement("ruby"); - const rb = document.createElement("span"); // rb element is deprecated in HTML - rb.textContent = "\u200B"; - - ruby.appendChild(rb); + ruby.appendChild(base); const rt = document.createElement("rt"); rt.style[RUBYPOSITION_PROP] = pos; @@ -1025,22 +1016,23 @@ function applyRubyReserve(lineList, context) { /* copy specified style properties from the sibling ruby container */ - if (copy_styles_from !== null) { - - for (let k = 0; k < copy_styles_from.style.length; k++) { - - ruby.style.setProperty(copy_styles_from.style.item(k), copy_styles_from.style.getPropertyValue(copy_styles_from.style.item(k))); + if (ruby_sibling !== null) { + for (let k = 0; k < ruby_sibling.style.length; k++) { + ruby.style.setProperty(ruby_sibling.style.item(k), ruby_sibling.style.getPropertyValue(ruby_sibling.style.item(k))); } } ruby.style[RUBYPOSITION_PROP] = pos; // for Chrome - sib.parentElement.insertBefore(ruby, sib); - + base = ruby; } + /* add before first span if there is no ruby sibling */ + const actual_sibling = ruby_sibling || lineList[i].elements[0].node; + actual_sibling.parentElement.insertBefore(base, actual_sibling); + } } From 05038747384ffd3d29e37ab2611d4f7397ab8583 Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Sat, 10 Oct 2026 08:44:23 -0700 Subject: [PATCH 4/6] Allow for mismatch between base and text elements --- src/main/js/html.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/js/html.js b/src/main/js/html.js index d390e56a..11712874 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -24,7 +24,7 @@ * POSSIBILITY OF SUCH DAMAGE. */ -import { reportError } from "./error.js"; +import { reportError, reportWarning } from "./error.js"; import { byName } from "./styles.js"; /** @@ -594,6 +594,9 @@ function processElement(context, dom_parent, isd_element, isd_parent) { * element that contains the ith ruby base and the ith ruby text of the * second text container; followed by * - the ith ruby text of the first text container. + * + * If a text container has more ruby texts than the base container has ruby + * bases, the extra ruby texts are kept and annotate an empty base. */ function processRubyContainer(context, ruby, isd_ruby) { @@ -615,11 +618,27 @@ function processRubyContainer(context, ruby, isd_ruby) { } + /* number of elements, i.e. the greater of the number of ruby bases and the number of ruby text in the first two text containers */ + + let ruby_count = bases.length; + + for (let k = 0; k < textContainers.length && k < 2; k++) { + + ruby_count = Math.max(ruby_count, textContainers[k].length); + + } + + if (ruby_count > bases.length) { + + reportWarning(context.errorHandler, "Ruby text container has more ruby text than the ruby base container has ruby bases."); + + } + const template = ruby.cloneNode(false); let outer = ruby; - for (let i = 0; i < bases.length; i++) { + for (let i = 0; i < ruby_count; i++) { if (i > 0) { @@ -641,7 +660,11 @@ function processRubyContainer(context, ruby, isd_ruby) { } - processElement(context, base_parent, bases[i], isd_ruby); + if (i < bases.length) { + + processElement(context, base_parent, bases[i], isd_ruby); + + } if (textContainers.length > 1 && i < textContainers[1].length) { From f2c4402d96829d9618a92d0e2996f94f420740f4 Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Sat, 10 Oct 2026 22:19:12 -0700 Subject: [PATCH 5/6] Add static site for comparisons --- README.md | 7 + package.json | 1 + src/test/script/gen-compare-site.mjs | 404 +++++++++++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 src/test/script/gen-compare-site.mjs diff --git a/README.md b/README.md index 600d6906..5456e05f 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,13 @@ pipenv install pipenv run python3 src/test/script/compare_renders.py ``` +[src/test/script/gen-compare-site.mjs](src/test/script/gen-compare-site.mjs) generates, under `build/compare`, a static website that compares imscJS renders with the reference PNGs of the [W3C IMSC test suite](https://github.com/w3c/imsc-tests). + +```sh +npm run build +npm run gen-compare-site +``` + ### Unit tests Unit tests run using Node's built-in [test runner](https://nodejs.org/api/test.html) and are located at [src/test/js](src/test/js). They can be run with: diff --git a/package.json b/package.json index 30a6a4d5..0a572d1f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "gen-imsc1_1": "node src/test/script/gen-render-package.mjs imsc-tests/imsc1_1 build/renders-imsc1_1.zip", "gen-imsc1_3": "node src/test/script/gen-render-package.mjs imsc-tests/imsc1_3 build/renders-imsc1_3.zip", "gen-reference-files": "node src/test/script/gen-reference-files.mjs", + "gen-compare-site": "node src/test/script/gen-compare-site.mjs", "install:firefox": "puppeteer browsers install firefox", "lint": "eslint .", "fix": "eslint --fix src", diff --git a/src/test/script/gen-compare-site.mjs b/src/test/script/gen-compare-site.mjs new file mode 100644 index 00000000..0ac9f8db --- /dev/null +++ b/src/test/script/gen-compare-site.mjs @@ -0,0 +1,404 @@ +#!/usr/bin/env node +/* + * Copyright (c) Sandflow Consulting LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Generates a static website that compares the HTML renders generated by + * imscJS for the imsc-tests test suites with the reference PNGs included in + * imsc-tests. + * + * imscJS renders the events of a test live, when the test is selected, in the + * browser viewing the site, so that layout-dependent values (e.g. fillLineGap + * paddings) are computed using that browser's fonts. The reference PNG is shown next to the render and can be + * overlaid on top of it, either blended in difference mode or with adjustable + * opacity. + * + * TTML documents are embedded in the pages, since browsers do not allow + * fetch() from file:// pages, and the site can therefore be opened directly + * from disk. + * + * Requires dist/imsc.debug.js (npm run build). + * + * Usage: + * node src/test/script/gen-compare-site.mjs [imsc-tests/imsc1 imsc-tests/imsc1_1 ...] [--out=build/compare] + * + * Open /index.html in a browser. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { RESOURCES_DIR, IMSC_BUNDLE_PATH } from "./render-harness.mjs"; + +const DEFAULT_REFFILES_ROOTS = ["imsc-tests/imsc1", "imsc-tests/imsc1_1", "imsc-tests/imsc1_2", "imsc-tests/imsc1_3"]; + +/* Must match the dimensions of the reference PNGs */ +const RENDER_WIDTH = 640; +const RENDER_HEIGHT = 360; + +function escapeHTML(s) { + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +/* JSON that can be safely embedded in a + + +
+

imscJS / ${escapeHTML(suiteName)}

+ + + + +↑/↓: previous/next test +
+
+ +
+
+ + + +`; +} + +function indexPage(suites) { + const items = suites + .map(({ name, testCount }) => + `
  • ${escapeHTML(name)} ` + + `${testCount} tests
  • `) + .join("\n"); + + return ` + + + + +imscJS renders + + + +

    imscJS renders vs. imsc-tests reference PNGs

    +

    Generated ${escapeHTML(new Date().toISOString())}

    +
      +${items} +
    + + +`; +} + +function generateSuite(reffilesRoot, outDir) { + const suiteName = path.basename(reffilesRoot); + const suiteDir = path.join(outDir, suiteName); + const srcDir = path.join(RESOURCES_DIR, reffilesRoot); + + const testList = JSON.parse(fs.readFileSync(path.join(srcDir, "tests.json"), "utf8")); + + fs.rmSync(suiteDir, { recursive: true, force: true }); + + /* images referenced by the TTML documents are resolved relative to them */ + fs.cpSync(path.join(srcDir, "ttml"), path.join(suiteDir, "ttml"), { recursive: true }); + fs.cpSync(path.join(srcDir, "png"), path.join(suiteDir, "ref"), { recursive: true }); + + const tests = []; + + for (const finfo of testList) { + const name = finfo.name || path.basename(finfo.path).split(".")[0]; + + const refDir = path.join(srcDir, "png", name); + const refEvents = fs.existsSync(refDir) + ? fs.readdirSync(refDir).filter((f) => f.endsWith(".png")).map((f) => f.slice(0, -4)) + : []; + + tests.push({ + name, + path: finfo.path, + params: finfo.params || {}, + ttml: fs.readFileSync(path.join(srcDir, "ttml", finfo.path), "utf8"), + ttmlDir: "ttml/" + path.posix.dirname(finfo.path) + "/", + refDir: "ref/" + name + "/", + refEvents, + }); + } + + writeFile(path.join(suiteDir, "index.html"), suitePage(suiteName, tests)); + + console.log(`Wrote ${suiteDir} (${tests.length} tests)`); + + return { name: suiteName, testCount: tests.length }; +} + +function main() { + if (!fs.existsSync(IMSC_BUNDLE_PATH)) { + throw new Error(`${IMSC_BUNDLE_PATH} does not exist.`); + } + + const args = process.argv.slice(2).filter((a) => !a.startsWith("--")); + const reffilesRoots = args.length ? args : DEFAULT_REFFILES_ROOTS; + + const outArg = process.argv.find((a) => a.startsWith("--out=")); + const outDir = path.resolve(outArg ? outArg.split("=")[1] : "build/compare"); + + fs.mkdirSync(outDir, { recursive: true }); + fs.copyFileSync(IMSC_BUNDLE_PATH, path.join(outDir, "imsc.debug.js")); + if (fs.existsSync(IMSC_BUNDLE_PATH + ".map")) { + fs.copyFileSync(IMSC_BUNDLE_PATH + ".map", path.join(outDir, "imsc.debug.js.map")); + } + + const suites = reffilesRoots.map((reffilesRoot) => generateSuite(reffilesRoot, outDir)); + + writeFile(path.join(outDir, "index.html"), indexPage(suites)); + + console.log(`Open ${path.join(outDir, "index.html")}`); +} + +main(); From 7584efc7ce7c653521727c4553d9d17a405a32c8 Mon Sep 17 00:00:00 2001 From: Pierre-Anthony Lemieux Date: Sun, 11 Oct 2026 11:36:58 -0700 Subject: [PATCH 6/6] Do not create spans in rt --- src/main/js/html.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/js/html.js b/src/main/js/html.js index 11712874..752ea2e7 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -388,10 +388,11 @@ function processElement(context, dom_parent, isd_element, isd_parent) { } - if (byName.textCombine.qname in isd_element.styleAttrs && - isd_element.styleAttrs[byName.textCombine.qname] === "all") { + if ((byName.textCombine.qname in isd_element.styleAttrs && + isd_element.styleAttrs[byName.textCombine.qname] === "all") || + e.closest("rt") !== null) { - /* ignore tate-chu-yoku since line break cannot happen within */ + /* ignore tate-chu-yoku and ruby text since line break cannot happen within */ e.textContent = isd_element.text; e._isd_element = isd_element;