asciidoctor-rhrev 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -182,29 +182,134 @@ function rollupTableCellRevisionEntries(catalog, doc, prefix) {
182
182
  })
183
183
  }
184
184
 
185
- // The cell's own <td>/<th> never carries the rhrev-changed role: that
186
- // needs a converter-level override, the same way the Ruby gem's HTML5
187
- // converter is overridden. Built and verified working standalone (a real
188
- // Opal subclass of Html5Converter, registered via ConverterFactory,
189
- // delegating to super for everything but a marked table), but it broke
190
- // real Antora builds: this exact demo site has three separate,
191
- // non-deduped @asciidoctor/core installs (rhrev, the site itself, and
192
- // baiyibai-extensions, whose cite-it extension wraps Antora's own document
193
- // loading), one of them (2.2.8 vs 2.2.9 elsewhere) a different version.
194
- // Antora's own converter/html5.js reads whatever is currently registered
195
- // as the 'html5' converter and builds a real Opal subclass on top of it;
196
- // with three non-identical copies of the Opal runtime involved, that
197
- // crashed (Object.setPrototypeOf called on null or undefined), confirmed
198
- // against a real local build, not assumed. That is a dependency-hygiene
199
- // problem in a sibling package, out of scope here.
185
+ // The cell's own <td>/<th> DOES carry the rhrev-changed role, via a
186
+ // converter-level override, patched directly onto the SAME live converter
187
+ // class object Antora's own pipeline is already using for this document
188
+ // (doc.getConverter().$$class, reached through Asciidoctor's own public
189
+ // API), never a separately-required or separately-registered class.
200
190
  //
201
- // Instead, propagateCellContentMarks above tags the cell's marked nested
202
- // block directly (a normal rollup-context node, so it needs no converter
203
- // override at all, the same unmodified converter already supports a role
204
- // there), rather than the cell itself. The visible result is a border
205
- // around the changed paragraph inside the cell rather than around the
206
- // whole cell box; confirmed against the real, unmodified converter output,
207
- // not assumed to work the same as the PDF gem's margin bar.
191
+ // EARLIER ATTEMPT, SUPERSEDED (kept here since the next engineer will
192
+ // otherwise reach for the same idea first): a real Opal subclass of
193
+ // Html5Converter, registered via ConverterFactory, the same way the Ruby
194
+ // gem's PDF converter is overridden. Built and verified working
195
+ // standalone, but broke real Antora builds: this exact demo site had
196
+ // three separate, non-deduped @asciidoctor/core installs (rhrev, the site
197
+ // itself, and baiyibai-extensions, whose cite-it extension wraps Antora's
198
+ // own document loading), one of them a different version. Antora's own
199
+ // converter/html5.js reads whatever is currently registered as the
200
+ // 'html5' converter and builds a real Opal subclass on top of that; with
201
+ // non-identical copies of the Opal runtime involved, defining a SECOND,
202
+ // independent subclass crashed (Object.setPrototypeOf called on null or
203
+ // undefined), confirmed against a real local build, not assumed. A
204
+ // require('@antora/asciidoc-loader') from inside this file cannot avoid
205
+ // that risk either, even now that baiyibai-extensions' own version drift
206
+ // is fixed (1.15.1): this package ships its own @asciidoctor/core copy
207
+ // for its own test suite, so a plain require() here resolves against
208
+ // THIS package's node_modules first, not necessarily the consuming
209
+ // site's, and remains a live, standing risk of resolving a different
210
+ // copy regardless of what any sibling package currently pins.
211
+ //
212
+ // doc.getConverter() sidesteps the whole class of problem: it returns
213
+ // whichever converter object is ACTUALLY resolved for THIS document, in
214
+ // THIS process, by Antora's own pipeline, guaranteed correct by
215
+ // construction rather than by keeping multiple packages' pins in sync.
216
+ // Confirmed directly: asciidoctor.load(text).getConverter() already
217
+ // returns a real, usable converter instance, with a real $$class, before
218
+ // convert() is ever called, meaning it is available in time from inside a
219
+ // treeprocessor (treeprocessors run during load(), before conversion).
220
+ const PATCHED_CONVERT_TABLE = Symbol.for('rhrev.patchedConvertTable')
221
+
222
+ // Idempotency is keyed on the converter CLASS, not the document: a single
223
+ // Antora build converts many documents against the same resolved
224
+ // converter class object, and redefining $convert_table would otherwise
225
+ // re-wrap the previous wrapper on every single page, nesting deeper each
226
+ // time (and, worse, the second pass would capture the FIRST WRAPPER as
227
+ // its own "original", not the real one, once the reference below is
228
+ // overwritten).
229
+ //
230
+ // Captures the real, original $convert_table function reference directly
231
+ // off the class's own prototype before replacing it, and calls that
232
+ // reference directly, rather than Opal.find_super_dispatcher (Antora's
233
+ // own html5.js pattern, copied from initially, then found wrong for this
234
+ // case, confirmed against a real crash: find_super_dispatcher climbs an
235
+ // inheritance chain, correct when Opal.klass creates a genuine SUBCLASS
236
+ // the way Antora's own converter/html5.js does, but this patches the
237
+ // SAME class object in place, no subclass involved, so there is no
238
+ // superclass boundary to dispatch across; the original implementation
239
+ // lives directly on classDef.$$prototype itself, confirmed directly, and
240
+ // Opal.defn below overwrites that exact slot).
241
+ function patchConvertTableForCellTagging (doc) {
242
+ const Opal = global.Opal
243
+ if (!Opal) return
244
+ const converter = doc.getConverter && doc.getConverter()
245
+ const classDef = converter && converter.$$class
246
+ if (!classDef || classDef[PATCHED_CONVERT_TABLE]) return
247
+ const originalConvertTable = classDef.$$prototype['$convert_table']
248
+ if (typeof originalConvertTable !== 'function') return
249
+ classDef[PATCHED_CONVERT_TABLE] = true
250
+
251
+ Opal.defn(classDef, '$convert_table', function convertTableWithCellTags (node) {
252
+ const html = originalConvertTable.call(this, node)
253
+ return typeof html === 'string' ? liftRhrevChangedToCells(html) : html
254
+ })
255
+ }
256
+
257
+ // Moves the rhrev-changed marker from a marked cell's own nested content
258
+ // (propagateCellContentMarks above) onto its enclosing <td>/<th>, so the
259
+ // site stylesheet highlights the whole cell box, matching the PDF
260
+ // backend's own margin bar spanning the cell's full row height, rather
261
+ // than only the changed paragraph inside it. Operates on the already-
262
+ // rendered HTML string, not the AST: Asciidoctor's real convert_table has
263
+ // no per-cell hook to override cleanly (table rendering is one self-
264
+ // contained method, the same reason the PDF gem needed a Prawn-level
265
+ // patch rather than an ordinary converter override, confirmed against
266
+ // Asciidoctor's own source), so this calls the real, unmodified super()
267
+ // implementation first and only adjusts its output afterward.
268
+ //
269
+ // Finds the nearest PRECEDING <td/<th opening tag before each marker,
270
+ // confirmed against real rendered output (a plain cell's own <p
271
+ // class="tableblock"> vs an a| cell's <div class="content">, both
272
+ // verified). A table nested inside a cell, itself containing its own
273
+ // <td>/<th> tags between the marker and its true enclosing cell, would
274
+ // defeat that heuristic: a known, accepted limitation, not handled.
275
+ function liftRhrevChangedToCells (html) {
276
+ const cellTagRx = /<(td|th)\b[^>]*>/g
277
+ const cellTags = []
278
+ let m
279
+ while ((m = cellTagRx.exec(html))) {
280
+ cellTags.push({ start: m.index, end: m.index + m[0].length, text: m[0] })
281
+ }
282
+ if (cellTags.length === 0) return html
283
+
284
+ const markerRx = /class="[^"]*\brhrev-changed\b[^"]*"/g
285
+ const toPatch = new Set()
286
+ while ((m = markerRx.exec(html))) {
287
+ let owner
288
+ for (const tag of cellTags) {
289
+ if (tag.start <= m.index) owner = tag
290
+ else break
291
+ }
292
+ if (owner) toPatch.add(owner)
293
+ }
294
+ if (toPatch.size === 0) return html
295
+
296
+ let out = ''
297
+ let cursor = 0
298
+ for (const tag of cellTags) {
299
+ if (!toPatch.has(tag)) continue
300
+ out += html.slice(cursor, tag.start)
301
+ out += addRoleClass(tag.text, 'rhrev-changed')
302
+ cursor = tag.end
303
+ }
304
+ out += html.slice(cursor)
305
+ return out
306
+ }
307
+
308
+ function addRoleClass (tagText, role) {
309
+ if (new RegExp(`\\bclass="[^"]*\\b${role}\\b[^"]*"`).test(tagText)) return tagText
310
+ if (/\bclass="/.test(tagText)) return tagText.replace(/\bclass="/, `class="${role} `)
311
+ return tagText.replace(/^<(td|th)\b/, `<$1 class="${role}"`)
312
+ }
208
313
 
209
314
  // Catalog for storing revision entries
210
315
  class RevisionCatalog {
@@ -729,6 +834,17 @@ module.exports.register = function (registry, context = {}) {
729
834
  const changeAttr = prefix + String(revnumber).replace(/\./g, '-')
730
835
  const contexts = ROW_LEVEL_CONTEXTS.concat(ROLLUP_CONTEXTS)
731
836
 
837
+ // A marked table cell was already tagged on its own nested content by
838
+ // propagateCellContentMarks, in the earlier treeprocessor: that walk
839
+ // is scoped to each cell's own, structurally separate inner document,
840
+ // invisible to this treeprocessor's own outer-document findBy below,
841
+ // so `tagged` (which only reflects THIS loop) is never a reliable
842
+ // signal for whether a cell needs patching. Runs unconditionally once
843
+ // change bars are known enabled, before the tagged/return-early check,
844
+ // so a document whose only marked content is a table cell (no other
845
+ // block matched below) still gets its converter patched.
846
+ patchConvertTableForCellTagging(doc)
847
+
732
848
  let tagged = false
733
849
  doc.findBy((b) => contexts.includes(b.getContext())).forEach((block) => {
734
850
  const attrs = block.getAttributes()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asciidoctor-rhrev",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Revision history block macro extension for Asciidoctor.js and Antora (HTML5 backend)",
5
5
  "main": "antora-extension/antora-rhrev-asciidoc.js",
6
6
  "scripts": {