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/4] 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/4] 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/4] 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/4] 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) {