@asciidoctor/core 4.0.0 → 4.0.1

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.
@@ -1,4 +1,4 @@
1
- var version = "4.0.0";
1
+ var version = "4.0.1";
2
2
  const packageJson = {
3
3
  version: version};
4
4
 
@@ -1745,14 +1745,23 @@ Logger.BasicFormatter = class {
1745
1745
 
1746
1746
  Logger.AutoFormattingMessage = {
1747
1747
  /**
1748
- * Attach auto-formatting to any plain object carrying { text, source_location }.
1749
- * @param {{text: string, source_location?: string}} obj
1748
+ * Attach auto-formatting to any plain object carrying
1749
+ * { text, source_location, include_location }.
1750
+ *
1751
+ * The location(s) are rendered only by inspect()/toString() (used when a
1752
+ * stderr Logger formats the line); the structured `source_location` /
1753
+ * `include_location` remain on the object so a MemoryLogger can record them
1754
+ * on the resulting LogMessage without duplicating them inside `text`.
1755
+ * @param {{text: string, source_location?: any, include_location?: any}} obj
1750
1756
  * @returns {typeof obj} The same object with inspect() and toString() added.
1751
1757
  */
1752
1758
  attach(obj) {
1753
1759
  obj.inspect = function () {
1754
1760
  const sloc = this.source_location;
1755
- return sloc ? `${sloc}: ${this.text}` : this.text
1761
+ const iloc = this.include_location;
1762
+ let text = sloc ? `${sloc}: ${this.text}` : this.text;
1763
+ if (iloc) text += ` (${iloc})`;
1764
+ return text
1756
1765
  };
1757
1766
  obj.toString = obj.inspect;
1758
1767
  return obj
@@ -1763,27 +1772,45 @@ Logger.AutoFormattingMessage = {
1763
1772
 
1764
1773
  /** Wrapper stored by MemoryLogger; provides getSeverity/getText/getSourceLocation. */
1765
1774
  class LogMessage {
1775
+ /**
1776
+ * @param {string} severity - Severity label, e.g. 'ERROR'.
1777
+ * @param {string|{text: string, source_location?: import('./reader.js').Cursor}|null} message
1778
+ */
1766
1779
  constructor(severity, message) {
1767
1780
  this.message = message;
1781
+ /** @type {string} */
1768
1782
  this.severity = severity; // string label, e.g. 'ERROR'
1769
1783
  // AutoFormattingMessage objects carry { text, source_location }
1770
1784
  if (message !== null && typeof message === 'object' && 'text' in message) {
1771
- this._text = message.text;
1772
- this._sourceLocation = message.source_location ?? null;
1785
+ /** @type {string} */
1786
+ this.text = message.text;
1787
+ /** @type {import('./reader.js').Cursor|null} */
1788
+ this.sourceLocation = message.source_location ?? null;
1773
1789
  } else {
1774
- this._text = message != null ? String(message) : '';
1775
- this._sourceLocation = null;
1790
+ this.text = message != null ? String(message) : '';
1791
+ this.sourceLocation = null;
1776
1792
  }
1777
1793
  }
1778
1794
 
1795
+ /**
1796
+ * @returns {string} The severity label, e.g. 'ERROR'.
1797
+ */
1779
1798
  getSeverity() {
1780
1799
  return this.severity
1781
1800
  }
1801
+
1802
+ /**
1803
+ * @returns {string} The message text.
1804
+ */
1782
1805
  getText() {
1783
- return this._text
1806
+ return this.text
1784
1807
  }
1808
+
1809
+ /**
1810
+ * @returns {import('./reader.js').Cursor|undefined} The source location, if any.
1811
+ */
1785
1812
  getSourceLocation() {
1786
- return this._sourceLocation ?? undefined
1813
+ return this.sourceLocation ?? undefined
1787
1814
  }
1788
1815
  }
1789
1816
 
@@ -1796,6 +1823,7 @@ class MemoryLogger {
1796
1823
  // matching Ruby's MemoryLogger (level: UNKNOWN). The add() method stores all
1797
1824
  // messages unconditionally — level is only used by the isDebug() guard.
1798
1825
  this.level = Severity.UNKNOWN;
1826
+ /** @type {LogMessage[]} */
1799
1827
  this.messages = [];
1800
1828
  }
1801
1829
 
@@ -1803,6 +1831,9 @@ class MemoryLogger {
1803
1831
  return new MemoryLogger()
1804
1832
  }
1805
1833
 
1834
+ /**
1835
+ * @returns {LogMessage[]} The log messages recorded so far, in order.
1836
+ */
1806
1837
  getMessages() {
1807
1838
  return this.messages
1808
1839
  }
@@ -6268,7 +6299,7 @@ function resolveBrowserIncludePath(reader, target, attrlist) {
6268
6299
  // ── Rule 2: target is an absolute URL (http:// / https:// / …) ───────────
6269
6300
  } else if (isUriish(pTarget)) {
6270
6301
  const descends = pathResolver.descendsFrom(pTarget, baseDir);
6271
- if (descends === false && !doc.getAttribute('allow-uri-read')) {
6302
+ if (descends === false && !doc.hasAttribute('allow-uri-read')) {
6272
6303
  return reader.replaceNextLine(_linkReplacement(reader, target, attrlist))
6273
6304
  }
6274
6305
  incPath = relpath = pTarget;
@@ -6302,7 +6333,7 @@ function resolveBrowserIncludePath(reader, target, attrlist) {
6302
6333
  } else {
6303
6334
  // Nested include: context dir is an absolute URL.
6304
6335
  const ctxDescends = pathResolver.descendsFrom(ctxDir, baseDir);
6305
- if (ctxDescends !== false || doc.getAttribute('allow-uri-read')) {
6336
+ if (ctxDescends !== false || doc.hasAttribute('allow-uri-read')) {
6306
6337
  incPath = `${ctxDir}/${pTarget}`;
6307
6338
  relpath = ctxDescends !== false ? incPath.slice(ctxDescends) : pTarget;
6308
6339
  } else {
@@ -6966,22 +6997,34 @@ class Reader {
6966
6997
  }
6967
6998
 
6968
6999
  /** @param {string} msg @param {{ sourceLocation?: any, includeLocation?: any }} [opts] */
6969
- _logWarn(msg, { sourceLocation, includeLocation } = {}) {
6970
- let text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg;
6971
- if (includeLocation) text += ` (${includeLocation.lineInfo})`;
6972
- this.logger.warn(text);
7000
+ _logWarn(msg, opts = {}) {
7001
+ this.logger.warn(this._messageWithContext(msg, opts));
6973
7002
  }
6974
7003
  _logError(msg, opts = {}) {
6975
- let text = opts.sourceLocation
6976
- ? `${opts.sourceLocation.lineInfo}: ${msg}`
6977
- : msg;
6978
- if (opts.includeLocation) text += ` (${opts.includeLocation.lineInfo})`;
6979
- this.logger.error(text);
7004
+ this.logger.error(this._messageWithContext(msg, opts));
6980
7005
  }
6981
7006
  /** @param {string} msg @param {{ sourceLocation?: any }} [opts] */
6982
- _logInfo(msg, { sourceLocation } = {}) {
6983
- const text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg;
6984
- this.logger.info(text);
7007
+ _logInfo(msg, opts = {}) {
7008
+ this.logger.info(this._messageWithContext(msg, opts));
7009
+ }
7010
+
7011
+ /**
7012
+ * Build an auto-formatting message that keeps the cursor as a structured
7013
+ * source_location (rather than baking it into the text). When displayed by a
7014
+ * stderr Logger the location is rendered as a "<path>: line <N>: " prefix, but
7015
+ * a MemoryLogger records it separately on the LogMessage so consumers can call
7016
+ * getSourceLocation(). Mirrors Ruby's Logging#message_with_context.
7017
+ * @param {string} msg
7018
+ * @param {{ sourceLocation?: any, includeLocation?: any }} [opts]
7019
+ * @internal
7020
+ */
7021
+ _messageWithContext(msg, { sourceLocation, includeLocation } = {}) {
7022
+ if (!sourceLocation && !includeLocation) return msg
7023
+ return Logger.AutoFormattingMessage.attach({
7024
+ text: msg,
7025
+ source_location: sourceLocation ?? null,
7026
+ include_location: includeLocation ?? null,
7027
+ })
6985
7028
  }
6986
7029
  }
6987
7030
 
@@ -7739,7 +7782,7 @@ class PreprocessorReader extends Reader {
7739
7782
  }
7740
7783
 
7741
7784
  if (isUriish(target) || typeof this._dir !== 'string') {
7742
- if (!doc.getAttribute('allow-uri-read')) {
7785
+ if (!doc.hasAttribute('allow-uri-read')) {
7743
7786
  this._logWarn(
7744
7787
  `cannot include contents of URI: ${target} (allow-uri-read attribute not enabled)`,
7745
7788
  { sourceLocation: this.cursor }
@@ -10349,7 +10392,12 @@ class Parser {
10349
10392
  }
10350
10393
  }
10351
10394
  (intro ?? section).blocks.push(newBlock);
10352
- for (const key of Object.keys(attributes)) delete attributes[key];
10395
+ // Reset the shared attributes object for the next block. Use Reflect.ownKeys
10396
+ // (not Object.keys) so the Symbol-keyed attribute entries (ATTR_ENTRIES_KEY)
10397
+ // are cleared too; otherwise the array of AttributeEntry objects leaks and
10398
+ // accumulates across blocks, causing reassigned attributes (e.g. a body-level
10399
+ // `:name:` redefined later) to all resolve to the final value at playback time.
10400
+ for (const key of Reflect.ownKeys(attributes)) delete attributes[key];
10353
10401
  }
10354
10402
  }
10355
10403
 
@@ -11196,7 +11244,13 @@ class Parser {
11196
11244
  }
11197
11245
  }
11198
11246
 
11199
- if (Object.keys(attributes).length > 0) block.updateAttributes(attributes);
11247
+ // Reflect.ownKeys (not Object.keys) so a block carrying only Symbol-keyed
11248
+ // attribute entries (ATTR_ENTRIES_KEY) — e.g. an `:attr:` entry immediately
11249
+ // preceding a list or table — still receives them, matching Ruby's
11250
+ // `attributes.empty?` where the `:attribute_entries` key is counted. Without this
11251
+ // the entries are dropped and the attribute is not played back for that block.
11252
+ if (Reflect.ownKeys(attributes).length > 0)
11253
+ block.updateAttributes(attributes);
11200
11254
  block.commitSubs();
11201
11255
 
11202
11256
  if (block.hasSub('callouts')) {
@@ -16224,8 +16278,16 @@ class Document extends AbstractBlock {
16224
16278
  }
16225
16279
 
16226
16280
  // Pre-compute all async text values (titles, list item text, cell text, reftexts)
16227
- // so that synchronous getters work correctly during conversion.
16281
+ // so that synchronous getters work correctly during conversion. _resolveAllTexts
16282
+ // replays attribute entries in document order (mirroring conversion) so body-level
16283
+ // attribute (re)assignments are in scope; snapshot and restore the document
16284
+ // attributes so the downstream steps and conversion still start from the restored
16285
+ // (header) state, matching Ruby's restore_attributes-before-convert invariant.
16286
+ const attributesSnapshot = { ...this.attributes };
16228
16287
  await this._resolveAllTexts(this);
16288
+ for (const key of Reflect.ownKeys(this.attributes))
16289
+ delete this.attributes[key];
16290
+ Object.assign(this.attributes, attributesSnapshot);
16229
16291
  // Reset the footnote counter so that body-content footnotes (processed during conversion)
16230
16292
  // start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
16231
16293
  // numbered during parsing via apply_title_subs, then the counter restarts for body content.
@@ -17299,6 +17361,12 @@ class Document extends AbstractBlock {
17299
17361
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
17300
17362
  */
17301
17363
  async _resolveAllTexts(block) {
17364
+ // Replay this block's attribute entries (in document order, since the walk is
17365
+ // depth-first pre-order like conversion) so that body-level attribute assignments
17366
+ // and reassignments are in scope when the block's — and its descendants' and later
17367
+ // siblings' — title / list item / table cell / reftext values are substituted.
17368
+ // Mirrors AbstractBlock#convert, which calls playbackAttributes before converting.
17369
+ this.playbackAttributes(block.attributes);
17302
17370
  // The header section lives outside document.blocks; pre-compute its title here so
17303
17371
  // that doc.doctitle() returns the fully-substituted title (with replacements applied,
17304
17372
  // e.g. ' → &#8217;) rather than the header-subs-only fallback.
@@ -18027,7 +18095,7 @@ const Substitutors = {
18027
18095
  *
18028
18096
  * @param {string|string[]} text - The text to process; must not be null.
18029
18097
  * @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
18030
- * @returns {string|string[]} Text with substitutions applied.
18098
+ * @returns {Promise<string|string[]>} Text with substitutions applied.
18031
18099
  */
18032
18100
  async applySubs(text, subs = NORMAL_SUBS) {
18033
18101
  const isEmpty = Array.isArray(text) ? text.length === 0 : text.length === 0;
@@ -18141,7 +18209,7 @@ const Substitutors = {
18141
18209
  * Substitute quoted text (emphasis, strong, monospaced, etc.)
18142
18210
  *
18143
18211
  * @param {string} text
18144
- * @returns {string}
18212
+ * @returns {Promise<string>}
18145
18213
  */
18146
18214
  async subQuotes(text) {
18147
18215
  const compat = this.document.compatMode;
@@ -18313,7 +18381,7 @@ const Substitutors = {
18313
18381
  * Substitute inline macros (links, images, etc.)
18314
18382
  *
18315
18383
  * @param {string} text
18316
- * @returns {string}
18384
+ * @returns {Promise<string>}
18317
18385
  */
18318
18386
  async subMacros(text) {
18319
18387
  const foundSquareBracket = text.includes('[');
@@ -19208,7 +19276,7 @@ const Substitutors = {
19208
19276
  * Substitute post replacements (hard line breaks).
19209
19277
  *
19210
19278
  * @param {string} text
19211
- * @returns {string}
19279
+ * @returns {Promise<string>}
19212
19280
  */
19213
19281
  async subPostReplacements(text) {
19214
19282
  if (
@@ -19246,7 +19314,7 @@ const Substitutors = {
19246
19314
  *
19247
19315
  * @param {string} source
19248
19316
  * @param {boolean} processCallouts
19249
- * @returns {string}
19317
+ * @returns {Promise<string>}
19250
19318
  */
19251
19319
  async subSource(source, processCallouts) {
19252
19320
  return processCallouts
@@ -19258,7 +19326,7 @@ const Substitutors = {
19258
19326
  * Substitute callout source references.
19259
19327
  *
19260
19328
  * @param {string} text
19261
- * @returns {string}
19329
+ * @returns {Promise<string>}
19262
19330
  */
19263
19331
  async subCallouts(text) {
19264
19332
  const calloutRx = this.hasAttribute('line-comment')
@@ -19287,7 +19355,7 @@ const Substitutors = {
19287
19355
  *
19288
19356
  * @param {string} source
19289
19357
  * @param {boolean} processCallouts
19290
- * @returns {string}
19358
+ * @returns {Promise<string>}
19291
19359
  */
19292
19360
  async highlightSource(source, processCallouts) {
19293
19361
  const syntaxHl = this.document.syntaxHighlighter;
@@ -19633,7 +19701,7 @@ const Substitutors = {
19633
19701
  * Restore passthrough text by reinserting into placeholder positions.
19634
19702
  *
19635
19703
  * @param {string} text
19636
- * @returns {string}
19704
+ * @returns {Promise<string>}
19637
19705
  */
19638
19706
  async restorePassthroughs(text) {
19639
19707
  if (!text.includes(PASS_START)) return text
@@ -19825,7 +19893,7 @@ const Substitutors = {
19825
19893
  * @param {string} attrlist
19826
19894
  * @param {string[]} [posattrs=[]]
19827
19895
  * @param {Object} [opts={}]
19828
- * @returns {Object}
19896
+ * @returns {Promise<Object>}
19829
19897
  */
19830
19898
  async parseAttributes(attrlist, posattrs = [], opts = {}) {
19831
19899
  if (!attrlist || attrlist.length === 0) return {}
@@ -24769,4 +24837,4 @@ const manpage = /*#__PURE__*/Object.freeze({
24769
24837
  default: ManPageConverter
24770
24838
  });
24771
24839
 
24772
- export { AbstractBlock, AbstractNode, Author, Block, BlockMacroProcessor, BlockProcessor, ContentModel, ConverterBase, CustomFactory$1 as ConverterCustomFactory, Converter as ConverterFactory, Cursor, DefaultFactory$1 as DefaultConverterFactory, DefaultFactory as DefaultSyntaxHighlighterFactory, DocinfoProcessor, Document, DocumentTitle, Extensions, Footnote, Html5Converter, HttpCache, HttpCacheManager, ImageReference, IncludeProcessor, Inline, InlineMacroProcessor, List, ListItem, Logger, LoggerManager, MemoryHttpCache, MemoryLogger, NullLogger, Postprocessor, Preprocessor, Processor, ProcessorExtension, Reader, Registry, RevisionInfo, SafeMode, Section, SyntaxHighlighter, SyntaxHighlighterBase, Timings, TreeProcessor, convert, deriveBackendTraits, getCoreVersion, getVersion, load };
24840
+ export { AbstractBlock, AbstractNode, Author, Block, BlockMacroProcessor, BlockProcessor, ContentModel, ConverterBase, CustomFactory$1 as ConverterCustomFactory, Converter as ConverterFactory, Cursor, DefaultFactory$1 as DefaultConverterFactory, DefaultFactory as DefaultSyntaxHighlighterFactory, DocinfoProcessor, Document, DocumentTitle, Extensions, Footnote, Html5Converter, HttpCache, HttpCacheManager, ImageReference, IncludeProcessor, Inline, InlineMacroProcessor, List, ListItem, LogMessage, Logger, LoggerManager, MemoryHttpCache, MemoryLogger, NullLogger, Postprocessor, Preprocessor, Processor, ProcessorExtension, Reader, Registry, RevisionInfo, SafeMode, Section, SyntaxHighlighter, SyntaxHighlighterBase, Timings, TreeProcessor, convert, deriveBackendTraits, getCoreVersion, getVersion, load };
@@ -5,7 +5,7 @@ const node_fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
7
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
- var version = "4.0.0";
8
+ var version = "4.0.1";
9
9
  const packageJson = {
10
10
  version: version};
11
11
 
@@ -1752,14 +1752,23 @@ Logger.BasicFormatter = class {
1752
1752
 
1753
1753
  Logger.AutoFormattingMessage = {
1754
1754
  /**
1755
- * Attach auto-formatting to any plain object carrying { text, source_location }.
1756
- * @param {{text: string, source_location?: string}} obj
1755
+ * Attach auto-formatting to any plain object carrying
1756
+ * { text, source_location, include_location }.
1757
+ *
1758
+ * The location(s) are rendered only by inspect()/toString() (used when a
1759
+ * stderr Logger formats the line); the structured `source_location` /
1760
+ * `include_location` remain on the object so a MemoryLogger can record them
1761
+ * on the resulting LogMessage without duplicating them inside `text`.
1762
+ * @param {{text: string, source_location?: any, include_location?: any}} obj
1757
1763
  * @returns {typeof obj} The same object with inspect() and toString() added.
1758
1764
  */
1759
1765
  attach(obj) {
1760
1766
  obj.inspect = function () {
1761
1767
  const sloc = this.source_location;
1762
- return sloc ? `${sloc}: ${this.text}` : this.text
1768
+ const iloc = this.include_location;
1769
+ let text = sloc ? `${sloc}: ${this.text}` : this.text;
1770
+ if (iloc) text += ` (${iloc})`;
1771
+ return text
1763
1772
  };
1764
1773
  obj.toString = obj.inspect;
1765
1774
  return obj
@@ -1770,27 +1779,45 @@ Logger.AutoFormattingMessage = {
1770
1779
 
1771
1780
  /** Wrapper stored by MemoryLogger; provides getSeverity/getText/getSourceLocation. */
1772
1781
  class LogMessage {
1782
+ /**
1783
+ * @param {string} severity - Severity label, e.g. 'ERROR'.
1784
+ * @param {string|{text: string, source_location?: import('./reader.js').Cursor}|null} message
1785
+ */
1773
1786
  constructor(severity, message) {
1774
1787
  this.message = message;
1788
+ /** @type {string} */
1775
1789
  this.severity = severity; // string label, e.g. 'ERROR'
1776
1790
  // AutoFormattingMessage objects carry { text, source_location }
1777
1791
  if (message !== null && typeof message === 'object' && 'text' in message) {
1778
- this._text = message.text;
1779
- this._sourceLocation = message.source_location ?? null;
1792
+ /** @type {string} */
1793
+ this.text = message.text;
1794
+ /** @type {import('./reader.js').Cursor|null} */
1795
+ this.sourceLocation = message.source_location ?? null;
1780
1796
  } else {
1781
- this._text = message != null ? String(message) : '';
1782
- this._sourceLocation = null;
1797
+ this.text = message != null ? String(message) : '';
1798
+ this.sourceLocation = null;
1783
1799
  }
1784
1800
  }
1785
1801
 
1802
+ /**
1803
+ * @returns {string} The severity label, e.g. 'ERROR'.
1804
+ */
1786
1805
  getSeverity() {
1787
1806
  return this.severity
1788
1807
  }
1808
+
1809
+ /**
1810
+ * @returns {string} The message text.
1811
+ */
1789
1812
  getText() {
1790
- return this._text
1813
+ return this.text
1791
1814
  }
1815
+
1816
+ /**
1817
+ * @returns {import('./reader.js').Cursor|undefined} The source location, if any.
1818
+ */
1792
1819
  getSourceLocation() {
1793
- return this._sourceLocation ?? undefined
1820
+ return this.sourceLocation ?? undefined
1794
1821
  }
1795
1822
  }
1796
1823
 
@@ -1803,6 +1830,7 @@ class MemoryLogger {
1803
1830
  // matching Ruby's MemoryLogger (level: UNKNOWN). The add() method stores all
1804
1831
  // messages unconditionally — level is only used by the isDebug() guard.
1805
1832
  this.level = Severity.UNKNOWN;
1833
+ /** @type {LogMessage[]} */
1806
1834
  this.messages = [];
1807
1835
  }
1808
1836
 
@@ -1810,6 +1838,9 @@ class MemoryLogger {
1810
1838
  return new MemoryLogger()
1811
1839
  }
1812
1840
 
1841
+ /**
1842
+ * @returns {LogMessage[]} The log messages recorded so far, in order.
1843
+ */
1813
1844
  getMessages() {
1814
1845
  return this.messages
1815
1846
  }
@@ -6275,7 +6306,7 @@ function resolveBrowserIncludePath(reader, target, attrlist) {
6275
6306
  // ── Rule 2: target is an absolute URL (http:// / https:// / …) ───────────
6276
6307
  } else if (isUriish(pTarget)) {
6277
6308
  const descends = pathResolver.descendsFrom(pTarget, baseDir);
6278
- if (descends === false && !doc.getAttribute('allow-uri-read')) {
6309
+ if (descends === false && !doc.hasAttribute('allow-uri-read')) {
6279
6310
  return reader.replaceNextLine(_linkReplacement(reader, target, attrlist))
6280
6311
  }
6281
6312
  incPath = relpath = pTarget;
@@ -6309,7 +6340,7 @@ function resolveBrowserIncludePath(reader, target, attrlist) {
6309
6340
  } else {
6310
6341
  // Nested include: context dir is an absolute URL.
6311
6342
  const ctxDescends = pathResolver.descendsFrom(ctxDir, baseDir);
6312
- if (ctxDescends !== false || doc.getAttribute('allow-uri-read')) {
6343
+ if (ctxDescends !== false || doc.hasAttribute('allow-uri-read')) {
6313
6344
  incPath = `${ctxDir}/${pTarget}`;
6314
6345
  relpath = ctxDescends !== false ? incPath.slice(ctxDescends) : pTarget;
6315
6346
  } else {
@@ -6973,22 +7004,34 @@ class Reader {
6973
7004
  }
6974
7005
 
6975
7006
  /** @param {string} msg @param {{ sourceLocation?: any, includeLocation?: any }} [opts] */
6976
- _logWarn(msg, { sourceLocation, includeLocation } = {}) {
6977
- let text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg;
6978
- if (includeLocation) text += ` (${includeLocation.lineInfo})`;
6979
- this.logger.warn(text);
7007
+ _logWarn(msg, opts = {}) {
7008
+ this.logger.warn(this._messageWithContext(msg, opts));
6980
7009
  }
6981
7010
  _logError(msg, opts = {}) {
6982
- let text = opts.sourceLocation
6983
- ? `${opts.sourceLocation.lineInfo}: ${msg}`
6984
- : msg;
6985
- if (opts.includeLocation) text += ` (${opts.includeLocation.lineInfo})`;
6986
- this.logger.error(text);
7011
+ this.logger.error(this._messageWithContext(msg, opts));
6987
7012
  }
6988
7013
  /** @param {string} msg @param {{ sourceLocation?: any }} [opts] */
6989
- _logInfo(msg, { sourceLocation } = {}) {
6990
- const text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg;
6991
- this.logger.info(text);
7014
+ _logInfo(msg, opts = {}) {
7015
+ this.logger.info(this._messageWithContext(msg, opts));
7016
+ }
7017
+
7018
+ /**
7019
+ * Build an auto-formatting message that keeps the cursor as a structured
7020
+ * source_location (rather than baking it into the text). When displayed by a
7021
+ * stderr Logger the location is rendered as a "<path>: line <N>: " prefix, but
7022
+ * a MemoryLogger records it separately on the LogMessage so consumers can call
7023
+ * getSourceLocation(). Mirrors Ruby's Logging#message_with_context.
7024
+ * @param {string} msg
7025
+ * @param {{ sourceLocation?: any, includeLocation?: any }} [opts]
7026
+ * @internal
7027
+ */
7028
+ _messageWithContext(msg, { sourceLocation, includeLocation } = {}) {
7029
+ if (!sourceLocation && !includeLocation) return msg
7030
+ return Logger.AutoFormattingMessage.attach({
7031
+ text: msg,
7032
+ source_location: sourceLocation ?? null,
7033
+ include_location: includeLocation ?? null,
7034
+ })
6992
7035
  }
6993
7036
  }
6994
7037
 
@@ -7746,7 +7789,7 @@ class PreprocessorReader extends Reader {
7746
7789
  }
7747
7790
 
7748
7791
  if (isUriish(target) || typeof this._dir !== 'string') {
7749
- if (!doc.getAttribute('allow-uri-read')) {
7792
+ if (!doc.hasAttribute('allow-uri-read')) {
7750
7793
  this._logWarn(
7751
7794
  `cannot include contents of URI: ${target} (allow-uri-read attribute not enabled)`,
7752
7795
  { sourceLocation: this.cursor }
@@ -10356,7 +10399,12 @@ class Parser {
10356
10399
  }
10357
10400
  }
10358
10401
  (intro ?? section).blocks.push(newBlock);
10359
- for (const key of Object.keys(attributes)) delete attributes[key];
10402
+ // Reset the shared attributes object for the next block. Use Reflect.ownKeys
10403
+ // (not Object.keys) so the Symbol-keyed attribute entries (ATTR_ENTRIES_KEY)
10404
+ // are cleared too; otherwise the array of AttributeEntry objects leaks and
10405
+ // accumulates across blocks, causing reassigned attributes (e.g. a body-level
10406
+ // `:name:` redefined later) to all resolve to the final value at playback time.
10407
+ for (const key of Reflect.ownKeys(attributes)) delete attributes[key];
10360
10408
  }
10361
10409
  }
10362
10410
 
@@ -11203,7 +11251,13 @@ class Parser {
11203
11251
  }
11204
11252
  }
11205
11253
 
11206
- if (Object.keys(attributes).length > 0) block.updateAttributes(attributes);
11254
+ // Reflect.ownKeys (not Object.keys) so a block carrying only Symbol-keyed
11255
+ // attribute entries (ATTR_ENTRIES_KEY) — e.g. an `:attr:` entry immediately
11256
+ // preceding a list or table — still receives them, matching Ruby's
11257
+ // `attributes.empty?` where the `:attribute_entries` key is counted. Without this
11258
+ // the entries are dropped and the attribute is not played back for that block.
11259
+ if (Reflect.ownKeys(attributes).length > 0)
11260
+ block.updateAttributes(attributes);
11207
11261
  block.commitSubs();
11208
11262
 
11209
11263
  if (block.hasSub('callouts')) {
@@ -16231,8 +16285,16 @@ class Document extends AbstractBlock {
16231
16285
  }
16232
16286
 
16233
16287
  // Pre-compute all async text values (titles, list item text, cell text, reftexts)
16234
- // so that synchronous getters work correctly during conversion.
16288
+ // so that synchronous getters work correctly during conversion. _resolveAllTexts
16289
+ // replays attribute entries in document order (mirroring conversion) so body-level
16290
+ // attribute (re)assignments are in scope; snapshot and restore the document
16291
+ // attributes so the downstream steps and conversion still start from the restored
16292
+ // (header) state, matching Ruby's restore_attributes-before-convert invariant.
16293
+ const attributesSnapshot = { ...this.attributes };
16235
16294
  await this._resolveAllTexts(this);
16295
+ for (const key of Reflect.ownKeys(this.attributes))
16296
+ delete this.attributes[key];
16297
+ Object.assign(this.attributes, attributesSnapshot);
16236
16298
  // Reset the footnote counter so that body-content footnotes (processed during conversion)
16237
16299
  // start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
16238
16300
  // numbered during parsing via apply_title_subs, then the counter restarts for body content.
@@ -17306,6 +17368,12 @@ class Document extends AbstractBlock {
17306
17368
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
17307
17369
  */
17308
17370
  async _resolveAllTexts(block) {
17371
+ // Replay this block's attribute entries (in document order, since the walk is
17372
+ // depth-first pre-order like conversion) so that body-level attribute assignments
17373
+ // and reassignments are in scope when the block's — and its descendants' and later
17374
+ // siblings' — title / list item / table cell / reftext values are substituted.
17375
+ // Mirrors AbstractBlock#convert, which calls playbackAttributes before converting.
17376
+ this.playbackAttributes(block.attributes);
17309
17377
  // The header section lives outside document.blocks; pre-compute its title here so
17310
17378
  // that doc.doctitle() returns the fully-substituted title (with replacements applied,
17311
17379
  // e.g. ' → &#8217;) rather than the header-subs-only fallback.
@@ -18034,7 +18102,7 @@ const Substitutors = {
18034
18102
  *
18035
18103
  * @param {string|string[]} text - The text to process; must not be null.
18036
18104
  * @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
18037
- * @returns {string|string[]} Text with substitutions applied.
18105
+ * @returns {Promise<string|string[]>} Text with substitutions applied.
18038
18106
  */
18039
18107
  async applySubs(text, subs = NORMAL_SUBS) {
18040
18108
  const isEmpty = Array.isArray(text) ? text.length === 0 : text.length === 0;
@@ -18148,7 +18216,7 @@ const Substitutors = {
18148
18216
  * Substitute quoted text (emphasis, strong, monospaced, etc.)
18149
18217
  *
18150
18218
  * @param {string} text
18151
- * @returns {string}
18219
+ * @returns {Promise<string>}
18152
18220
  */
18153
18221
  async subQuotes(text) {
18154
18222
  const compat = this.document.compatMode;
@@ -18320,7 +18388,7 @@ const Substitutors = {
18320
18388
  * Substitute inline macros (links, images, etc.)
18321
18389
  *
18322
18390
  * @param {string} text
18323
- * @returns {string}
18391
+ * @returns {Promise<string>}
18324
18392
  */
18325
18393
  async subMacros(text) {
18326
18394
  const foundSquareBracket = text.includes('[');
@@ -19215,7 +19283,7 @@ const Substitutors = {
19215
19283
  * Substitute post replacements (hard line breaks).
19216
19284
  *
19217
19285
  * @param {string} text
19218
- * @returns {string}
19286
+ * @returns {Promise<string>}
19219
19287
  */
19220
19288
  async subPostReplacements(text) {
19221
19289
  if (
@@ -19253,7 +19321,7 @@ const Substitutors = {
19253
19321
  *
19254
19322
  * @param {string} source
19255
19323
  * @param {boolean} processCallouts
19256
- * @returns {string}
19324
+ * @returns {Promise<string>}
19257
19325
  */
19258
19326
  async subSource(source, processCallouts) {
19259
19327
  return processCallouts
@@ -19265,7 +19333,7 @@ const Substitutors = {
19265
19333
  * Substitute callout source references.
19266
19334
  *
19267
19335
  * @param {string} text
19268
- * @returns {string}
19336
+ * @returns {Promise<string>}
19269
19337
  */
19270
19338
  async subCallouts(text) {
19271
19339
  const calloutRx = this.hasAttribute('line-comment')
@@ -19294,7 +19362,7 @@ const Substitutors = {
19294
19362
  *
19295
19363
  * @param {string} source
19296
19364
  * @param {boolean} processCallouts
19297
- * @returns {string}
19365
+ * @returns {Promise<string>}
19298
19366
  */
19299
19367
  async highlightSource(source, processCallouts) {
19300
19368
  const syntaxHl = this.document.syntaxHighlighter;
@@ -19640,7 +19708,7 @@ const Substitutors = {
19640
19708
  * Restore passthrough text by reinserting into placeholder positions.
19641
19709
  *
19642
19710
  * @param {string} text
19643
- * @returns {string}
19711
+ * @returns {Promise<string>}
19644
19712
  */
19645
19713
  async restorePassthroughs(text) {
19646
19714
  if (!text.includes(PASS_START)) return text
@@ -19832,7 +19900,7 @@ const Substitutors = {
19832
19900
  * @param {string} attrlist
19833
19901
  * @param {string[]} [posattrs=[]]
19834
19902
  * @param {Object} [opts={}]
19835
- * @returns {Object}
19903
+ * @returns {Promise<Object>}
19836
19904
  */
19837
19905
  async parseAttributes(attrlist, posattrs = [], opts = {}) {
19838
19906
  if (!attrlist || attrlist.length === 0) return {}
@@ -25329,6 +25397,7 @@ exports.Inline = Inline;
25329
25397
  exports.InlineMacroProcessor = InlineMacroProcessor;
25330
25398
  exports.List = List;
25331
25399
  exports.ListItem = ListItem;
25400
+ exports.LogMessage = LogMessage;
25332
25401
  exports.Logger = Logger;
25333
25402
  exports.LoggerManager = LoggerManager;
25334
25403
  exports.MemoryHttpCache = MemoryHttpCache;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asciidoctor/core",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Asciidoctor.js: AsciiDoc in JavaScript powered by Asciidoctor",
5
5
  "type": "module",
6
6
  "main": "build/node/index.cjs",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "homepage": "https://github.com/asciidoctor/asciidoctor.js",
68
68
  "volta": {
69
- "node": "24.16.0"
69
+ "node": "24.18.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@biomejs/biome": "^2.4.13",
@@ -92,7 +92,7 @@ export function resolveBrowserIncludePath(reader, target, attrlist) {
92
92
  // ── Rule 2: target is an absolute URL (http:// / https:// / …) ───────────
93
93
  } else if (isUriish(pTarget)) {
94
94
  const descends = pathResolver.descendsFrom(pTarget, baseDir)
95
- if (descends === false && !doc.getAttribute('allow-uri-read')) {
95
+ if (descends === false && !doc.hasAttribute('allow-uri-read')) {
96
96
  return reader.replaceNextLine(_linkReplacement(reader, target, attrlist))
97
97
  }
98
98
  incPath = relpath = pTarget
@@ -126,7 +126,7 @@ export function resolveBrowserIncludePath(reader, target, attrlist) {
126
126
  } else {
127
127
  // Nested include: context dir is an absolute URL.
128
128
  const ctxDescends = pathResolver.descendsFrom(ctxDir, baseDir)
129
- if (ctxDescends !== false || doc.getAttribute('allow-uri-read')) {
129
+ if (ctxDescends !== false || doc.hasAttribute('allow-uri-read')) {
130
130
  incPath = `${ctxDir}/${pTarget}`
131
131
  relpath = ctxDescends !== false ? incPath.slice(ctxDescends) : pTarget
132
132
  } else {
package/src/browser.js CHANGED
@@ -9,7 +9,13 @@ import {
9
9
  ImageReference,
10
10
  RevisionInfo,
11
11
  } from './document.js'
12
- import { Logger, LoggerManager, MemoryLogger, NullLogger } from './logging.js'
12
+ import {
13
+ Logger,
14
+ LoggerManager,
15
+ LogMessage,
16
+ MemoryLogger,
17
+ NullLogger,
18
+ } from './logging.js'
13
19
  import { HttpCache, MemoryHttpCache, HttpCacheManager } from './http_cache.js'
14
20
  import { SafeMode, ContentModel } from './constants.js'
15
21
  import { Timings } from './timings.js'
@@ -110,6 +116,7 @@ export {
110
116
  Reader,
111
117
  SyntaxHighlighterBase,
112
118
  LoggerManager,
119
+ LogMessage,
113
120
  MemoryLogger,
114
121
  NullLogger,
115
122
  HttpCache,
package/src/document.js CHANGED
@@ -587,8 +587,16 @@ export class Document extends AbstractBlock {
587
587
  }
588
588
 
589
589
  // Pre-compute all async text values (titles, list item text, cell text, reftexts)
590
- // so that synchronous getters work correctly during conversion.
590
+ // so that synchronous getters work correctly during conversion. _resolveAllTexts
591
+ // replays attribute entries in document order (mirroring conversion) so body-level
592
+ // attribute (re)assignments are in scope; snapshot and restore the document
593
+ // attributes so the downstream steps and conversion still start from the restored
594
+ // (header) state, matching Ruby's restore_attributes-before-convert invariant.
595
+ const attributesSnapshot = { ...this.attributes }
591
596
  await this._resolveAllTexts(this)
597
+ for (const key of Reflect.ownKeys(this.attributes))
598
+ delete this.attributes[key]
599
+ Object.assign(this.attributes, attributesSnapshot)
592
600
  // Reset the footnote counter so that body-content footnotes (processed during conversion)
593
601
  // start numbering from 1, reproducing Ruby's "out of sequence" quirk: title footnotes are
594
602
  // numbered during parsing via apply_title_subs, then the counter restarts for body content.
@@ -1662,6 +1670,12 @@ export class Document extends AbstractBlock {
1662
1670
  * Handles titles (AbstractBlock), list item text, table cell text, and reftexts.
1663
1671
  */
1664
1672
  async _resolveAllTexts(block) {
1673
+ // Replay this block's attribute entries (in document order, since the walk is
1674
+ // depth-first pre-order like conversion) so that body-level attribute assignments
1675
+ // and reassignments are in scope when the block's — and its descendants' and later
1676
+ // siblings' — title / list item / table cell / reftext values are substituted.
1677
+ // Mirrors AbstractBlock#convert, which calls playbackAttributes before converting.
1678
+ this.playbackAttributes(block.attributes)
1665
1679
  // The header section lives outside document.blocks; pre-compute its title here so
1666
1680
  // that doc.doctitle() returns the fully-substituted title (with replacements applied,
1667
1681
  // e.g. ' → &#8217;) rather than the header-subs-only fallback.
package/src/index.js CHANGED
@@ -9,7 +9,13 @@ import {
9
9
  ImageReference,
10
10
  RevisionInfo,
11
11
  } from './document.js'
12
- import { Logger, LoggerManager, MemoryLogger, NullLogger } from './logging.js'
12
+ import {
13
+ Logger,
14
+ LoggerManager,
15
+ LogMessage,
16
+ MemoryLogger,
17
+ NullLogger,
18
+ } from './logging.js'
13
19
  import { HttpCache, MemoryHttpCache, HttpCacheManager } from './http_cache.js'
14
20
  import { SafeMode, ContentModel } from './constants.js'
15
21
  import { Timings } from './timings.js'
@@ -122,6 +128,7 @@ export {
122
128
  Reader,
123
129
  SyntaxHighlighterBase,
124
130
  LoggerManager,
131
+ LogMessage,
125
132
  MemoryLogger,
126
133
  NullLogger,
127
134
  HttpCache,
package/src/logging.js CHANGED
@@ -277,14 +277,23 @@ Logger.BasicFormatter = class {
277
277
 
278
278
  Logger.AutoFormattingMessage = {
279
279
  /**
280
- * Attach auto-formatting to any plain object carrying { text, source_location }.
281
- * @param {{text: string, source_location?: string}} obj
280
+ * Attach auto-formatting to any plain object carrying
281
+ * { text, source_location, include_location }.
282
+ *
283
+ * The location(s) are rendered only by inspect()/toString() (used when a
284
+ * stderr Logger formats the line); the structured `source_location` /
285
+ * `include_location` remain on the object so a MemoryLogger can record them
286
+ * on the resulting LogMessage without duplicating them inside `text`.
287
+ * @param {{text: string, source_location?: any, include_location?: any}} obj
282
288
  * @returns {typeof obj} The same object with inspect() and toString() added.
283
289
  */
284
290
  attach(obj) {
285
291
  obj.inspect = function () {
286
292
  const sloc = this.source_location
287
- return sloc ? `${sloc}: ${this.text}` : this.text
293
+ const iloc = this.include_location
294
+ let text = sloc ? `${sloc}: ${this.text}` : this.text
295
+ if (iloc) text += ` (${iloc})`
296
+ return text
288
297
  }
289
298
  obj.toString = obj.inspect
290
299
  return obj
@@ -294,28 +303,46 @@ Logger.AutoFormattingMessage = {
294
303
  // ── LogMessage ────────────────────────────────────────────────────────────────
295
304
 
296
305
  /** Wrapper stored by MemoryLogger; provides getSeverity/getText/getSourceLocation. */
297
- class LogMessage {
306
+ export class LogMessage {
307
+ /**
308
+ * @param {string} severity - Severity label, e.g. 'ERROR'.
309
+ * @param {string|{text: string, source_location?: import('./reader.js').Cursor}|null} message
310
+ */
298
311
  constructor(severity, message) {
299
312
  this.message = message
313
+ /** @type {string} */
300
314
  this.severity = severity // string label, e.g. 'ERROR'
301
315
  // AutoFormattingMessage objects carry { text, source_location }
302
316
  if (message !== null && typeof message === 'object' && 'text' in message) {
303
- this._text = message.text
304
- this._sourceLocation = message.source_location ?? null
317
+ /** @type {string} */
318
+ this.text = message.text
319
+ /** @type {import('./reader.js').Cursor|null} */
320
+ this.sourceLocation = message.source_location ?? null
305
321
  } else {
306
- this._text = message != null ? String(message) : ''
307
- this._sourceLocation = null
322
+ this.text = message != null ? String(message) : ''
323
+ this.sourceLocation = null
308
324
  }
309
325
  }
310
326
 
327
+ /**
328
+ * @returns {string} The severity label, e.g. 'ERROR'.
329
+ */
311
330
  getSeverity() {
312
331
  return this.severity
313
332
  }
333
+
334
+ /**
335
+ * @returns {string} The message text.
336
+ */
314
337
  getText() {
315
- return this._text
338
+ return this.text
316
339
  }
340
+
341
+ /**
342
+ * @returns {import('./reader.js').Cursor|undefined} The source location, if any.
343
+ */
317
344
  getSourceLocation() {
318
- return this._sourceLocation ?? undefined
345
+ return this.sourceLocation ?? undefined
319
346
  }
320
347
  }
321
348
 
@@ -328,6 +355,7 @@ export class MemoryLogger {
328
355
  // matching Ruby's MemoryLogger (level: UNKNOWN). The add() method stores all
329
356
  // messages unconditionally — level is only used by the isDebug() guard.
330
357
  this.level = Severity.UNKNOWN
358
+ /** @type {LogMessage[]} */
331
359
  this.messages = []
332
360
  }
333
361
 
@@ -335,6 +363,9 @@ export class MemoryLogger {
335
363
  return new MemoryLogger()
336
364
  }
337
365
 
366
+ /**
367
+ * @returns {LogMessage[]} The log messages recorded so far, in order.
368
+ */
338
369
  getMessages() {
339
370
  return this.messages
340
371
  }
package/src/parser.js CHANGED
@@ -598,7 +598,12 @@ export class Parser {
598
598
  }
599
599
  }
600
600
  ;(intro ?? section).blocks.push(newBlock)
601
- for (const key of Object.keys(attributes)) delete attributes[key]
601
+ // Reset the shared attributes object for the next block. Use Reflect.ownKeys
602
+ // (not Object.keys) so the Symbol-keyed attribute entries (ATTR_ENTRIES_KEY)
603
+ // are cleared too; otherwise the array of AttributeEntry objects leaks and
604
+ // accumulates across blocks, causing reassigned attributes (e.g. a body-level
605
+ // `:name:` redefined later) to all resolve to the final value at playback time.
606
+ for (const key of Reflect.ownKeys(attributes)) delete attributes[key]
602
607
  }
603
608
  }
604
609
 
@@ -1449,7 +1454,13 @@ export class Parser {
1449
1454
  }
1450
1455
  }
1451
1456
 
1452
- if (Object.keys(attributes).length > 0) block.updateAttributes(attributes)
1457
+ // Reflect.ownKeys (not Object.keys) so a block carrying only Symbol-keyed
1458
+ // attribute entries (ATTR_ENTRIES_KEY) — e.g. an `:attr:` entry immediately
1459
+ // preceding a list or table — still receives them, matching Ruby's
1460
+ // `attributes.empty?` where the `:attribute_entries` key is counted. Without this
1461
+ // the entries are dropped and the attribute is not played back for that block.
1462
+ if (Reflect.ownKeys(attributes).length > 0)
1463
+ block.updateAttributes(attributes)
1453
1464
  block.commitSubs()
1454
1465
 
1455
1466
  if (block.hasSub('callouts')) {
package/src/reader.js CHANGED
@@ -675,22 +675,34 @@ export class Reader {
675
675
  }
676
676
 
677
677
  /** @param {string} msg @param {{ sourceLocation?: any, includeLocation?: any }} [opts] */
678
- _logWarn(msg, { sourceLocation, includeLocation } = {}) {
679
- let text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg
680
- if (includeLocation) text += ` (${includeLocation.lineInfo})`
681
- this.logger.warn(text)
678
+ _logWarn(msg, opts = {}) {
679
+ this.logger.warn(this._messageWithContext(msg, opts))
682
680
  }
683
681
  _logError(msg, opts = {}) {
684
- let text = opts.sourceLocation
685
- ? `${opts.sourceLocation.lineInfo}: ${msg}`
686
- : msg
687
- if (opts.includeLocation) text += ` (${opts.includeLocation.lineInfo})`
688
- this.logger.error(text)
682
+ this.logger.error(this._messageWithContext(msg, opts))
689
683
  }
690
684
  /** @param {string} msg @param {{ sourceLocation?: any }} [opts] */
691
- _logInfo(msg, { sourceLocation } = {}) {
692
- const text = sourceLocation ? `${sourceLocation.lineInfo}: ${msg}` : msg
693
- this.logger.info(text)
685
+ _logInfo(msg, opts = {}) {
686
+ this.logger.info(this._messageWithContext(msg, opts))
687
+ }
688
+
689
+ /**
690
+ * Build an auto-formatting message that keeps the cursor as a structured
691
+ * source_location (rather than baking it into the text). When displayed by a
692
+ * stderr Logger the location is rendered as a "<path>: line <N>: " prefix, but
693
+ * a MemoryLogger records it separately on the LogMessage so consumers can call
694
+ * getSourceLocation(). Mirrors Ruby's Logging#message_with_context.
695
+ * @param {string} msg
696
+ * @param {{ sourceLocation?: any, includeLocation?: any }} [opts]
697
+ * @internal
698
+ */
699
+ _messageWithContext(msg, { sourceLocation, includeLocation } = {}) {
700
+ if (!sourceLocation && !includeLocation) return msg
701
+ return Logger.AutoFormattingMessage.attach({
702
+ text: msg,
703
+ source_location: sourceLocation ?? null,
704
+ include_location: includeLocation ?? null,
705
+ })
694
706
  }
695
707
  }
696
708
 
@@ -1448,7 +1460,7 @@ export class PreprocessorReader extends Reader {
1448
1460
  }
1449
1461
 
1450
1462
  if (isUriish(target) || typeof this._dir !== 'string') {
1451
- if (!doc.getAttribute('allow-uri-read')) {
1463
+ if (!doc.hasAttribute('allow-uri-read')) {
1452
1464
  this._logWarn(
1453
1465
  `cannot include contents of URI: ${target} (allow-uri-read attribute not enabled)`,
1454
1466
  { sourceLocation: this.cursor }
@@ -176,7 +176,7 @@ export const Substitutors = {
176
176
  *
177
177
  * @param {string|string[]} text - The text to process; must not be null.
178
178
  * @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
179
- * @returns {string|string[]} Text with substitutions applied.
179
+ * @returns {Promise<string|string[]>} Text with substitutions applied.
180
180
  */
181
181
  async applySubs(text, subs = NORMAL_SUBS) {
182
182
  const isEmpty = Array.isArray(text) ? text.length === 0 : text.length === 0
@@ -290,7 +290,7 @@ export const Substitutors = {
290
290
  * Substitute quoted text (emphasis, strong, monospaced, etc.)
291
291
  *
292
292
  * @param {string} text
293
- * @returns {string}
293
+ * @returns {Promise<string>}
294
294
  */
295
295
  async subQuotes(text) {
296
296
  const compat = this.document.compatMode
@@ -462,7 +462,7 @@ export const Substitutors = {
462
462
  * Substitute inline macros (links, images, etc.)
463
463
  *
464
464
  * @param {string} text
465
- * @returns {string}
465
+ * @returns {Promise<string>}
466
466
  */
467
467
  async subMacros(text) {
468
468
  const foundSquareBracket = text.includes('[')
@@ -1357,7 +1357,7 @@ export const Substitutors = {
1357
1357
  * Substitute post replacements (hard line breaks).
1358
1358
  *
1359
1359
  * @param {string} text
1360
- * @returns {string}
1360
+ * @returns {Promise<string>}
1361
1361
  */
1362
1362
  async subPostReplacements(text) {
1363
1363
  if (
@@ -1395,7 +1395,7 @@ export const Substitutors = {
1395
1395
  *
1396
1396
  * @param {string} source
1397
1397
  * @param {boolean} processCallouts
1398
- * @returns {string}
1398
+ * @returns {Promise<string>}
1399
1399
  */
1400
1400
  async subSource(source, processCallouts) {
1401
1401
  return processCallouts
@@ -1407,7 +1407,7 @@ export const Substitutors = {
1407
1407
  * Substitute callout source references.
1408
1408
  *
1409
1409
  * @param {string} text
1410
- * @returns {string}
1410
+ * @returns {Promise<string>}
1411
1411
  */
1412
1412
  async subCallouts(text) {
1413
1413
  const calloutRx = this.hasAttribute('line-comment')
@@ -1436,7 +1436,7 @@ export const Substitutors = {
1436
1436
  *
1437
1437
  * @param {string} source
1438
1438
  * @param {boolean} processCallouts
1439
- * @returns {string}
1439
+ * @returns {Promise<string>}
1440
1440
  */
1441
1441
  async highlightSource(source, processCallouts) {
1442
1442
  const syntaxHl = this.document.syntaxHighlighter
@@ -1782,7 +1782,7 @@ export const Substitutors = {
1782
1782
  * Restore passthrough text by reinserting into placeholder positions.
1783
1783
  *
1784
1784
  * @param {string} text
1785
- * @returns {string}
1785
+ * @returns {Promise<string>}
1786
1786
  */
1787
1787
  async restorePassthroughs(text) {
1788
1788
  if (!text.includes(PASS_START)) return text
@@ -1974,7 +1974,7 @@ export const Substitutors = {
1974
1974
  * @param {string} attrlist
1975
1975
  * @param {string[]} [posattrs=[]]
1976
1976
  * @param {Object} [opts={}]
1977
- * @returns {Object}
1977
+ * @returns {Promise<Object>}
1978
1978
  */
1979
1979
  async parseAttributes(attrlist, posattrs = [], opts = {}) {
1980
1980
  if (!attrlist || attrlist.length === 0) return {}
package/types/index.d.cts CHANGED
@@ -54,6 +54,7 @@ import { Section } from './section.js';
54
54
  import { Reader } from './reader.js';
55
55
  import { SyntaxHighlighterBase } from './syntax_highlighter.js';
56
56
  import { LoggerManager } from './logging.js';
57
+ import { LogMessage } from './logging.js';
57
58
  import { MemoryLogger } from './logging.js';
58
59
  import { NullLogger } from './logging.js';
59
60
  import { HttpCache } from './http_cache.js';
@@ -83,4 +84,4 @@ import { deriveBackendTraits } from './converter.js';
83
84
  import { DefaultFactory as DefaultSyntaxHighlighterFactory } from './syntax_highlighter.js';
84
85
  import Html5Converter from './converter/html5.js';
85
86
  import { SyntaxHighlighter } from './syntax_highlighter.js';
86
- export { convert, convertFile, Document, DocumentTitle, Author, Footnote, ImageReference, RevisionInfo, Logger, AbstractNode, AbstractBlock, Inline, Block, List, ListItem, Section, Reader, SyntaxHighlighterBase, LoggerManager, MemoryLogger, NullLogger, HttpCache, MemoryHttpCache, HttpCacheManager, SafeMode, ContentModel, Timings, Registry, Processor, ProcessorExtension, Preprocessor, TreeProcessor, Postprocessor, IncludeProcessor, DocinfoProcessor, BlockProcessor, InlineMacroProcessor, BlockMacroProcessor, Extensions, Cursor, Converter as ConverterFactory, ConverterBase, CustomFactory as ConverterCustomFactory, DefaultConverterFactory, deriveBackendTraits, DefaultSyntaxHighlighterFactory, Html5Converter, SyntaxHighlighter };
87
+ export { convert, convertFile, Document, DocumentTitle, Author, Footnote, ImageReference, RevisionInfo, Logger, AbstractNode, AbstractBlock, Inline, Block, List, ListItem, Section, Reader, SyntaxHighlighterBase, LoggerManager, LogMessage, MemoryLogger, NullLogger, HttpCache, MemoryHttpCache, HttpCacheManager, SafeMode, ContentModel, Timings, Registry, Processor, ProcessorExtension, Preprocessor, TreeProcessor, Postprocessor, IncludeProcessor, DocinfoProcessor, BlockProcessor, InlineMacroProcessor, BlockMacroProcessor, Extensions, Cursor, Converter as ConverterFactory, ConverterBase, CustomFactory as ConverterCustomFactory, DefaultConverterFactory, deriveBackendTraits, DefaultSyntaxHighlighterFactory, Html5Converter, SyntaxHighlighter };
package/types/index.d.ts CHANGED
@@ -53,6 +53,7 @@ import { Section } from './section.js';
53
53
  import { Reader } from './reader.js';
54
54
  import { SyntaxHighlighterBase } from './syntax_highlighter.js';
55
55
  import { LoggerManager } from './logging.js';
56
+ import { LogMessage } from './logging.js';
56
57
  import { MemoryLogger } from './logging.js';
57
58
  import { NullLogger } from './logging.js';
58
59
  import { HttpCache } from './http_cache.js';
@@ -82,4 +83,4 @@ import { deriveBackendTraits } from './converter.js';
82
83
  import { DefaultFactory as DefaultSyntaxHighlighterFactory } from './syntax_highlighter.js';
83
84
  import Html5Converter from './converter/html5.js';
84
85
  import { SyntaxHighlighter } from './syntax_highlighter.js';
85
- export { convert, convertFile, Document, DocumentTitle, Author, Footnote, ImageReference, RevisionInfo, Logger, AbstractNode, AbstractBlock, Inline, Block, List, ListItem, Section, Reader, SyntaxHighlighterBase, LoggerManager, MemoryLogger, NullLogger, HttpCache, MemoryHttpCache, HttpCacheManager, SafeMode, ContentModel, Timings, Registry, Processor, ProcessorExtension, Preprocessor, TreeProcessor, Postprocessor, IncludeProcessor, DocinfoProcessor, BlockProcessor, InlineMacroProcessor, BlockMacroProcessor, Extensions, Cursor, Converter as ConverterFactory, ConverterBase, CustomFactory as ConverterCustomFactory, DefaultConverterFactory, deriveBackendTraits, DefaultSyntaxHighlighterFactory, Html5Converter, SyntaxHighlighter };
86
+ export { convert, convertFile, Document, DocumentTitle, Author, Footnote, ImageReference, RevisionInfo, Logger, AbstractNode, AbstractBlock, Inline, Block, List, ListItem, Section, Reader, SyntaxHighlighterBase, LoggerManager, LogMessage, MemoryLogger, NullLogger, HttpCache, MemoryHttpCache, HttpCacheManager, SafeMode, ContentModel, Timings, Registry, Processor, ProcessorExtension, Preprocessor, TreeProcessor, Postprocessor, IncludeProcessor, DocinfoProcessor, BlockProcessor, InlineMacroProcessor, BlockMacroProcessor, Extensions, Cursor, Converter as ConverterFactory, ConverterBase, CustomFactory as ConverterCustomFactory, DefaultConverterFactory, deriveBackendTraits, DefaultSyntaxHighlighterFactory, Html5Converter, SyntaxHighlighter };
@@ -95,22 +95,66 @@ export namespace Logger {
95
95
  export { BasicFormatter };
96
96
  export namespace AutoFormattingMessage {
97
97
  /**
98
- * Attach auto-formatting to any plain object carrying { text, source_location }.
99
- * @param {{text: string, source_location?: string}} obj
98
+ * Attach auto-formatting to any plain object carrying
99
+ * { text, source_location, include_location }.
100
+ *
101
+ * The location(s) are rendered only by inspect()/toString() (used when a
102
+ * stderr Logger formats the line); the structured `source_location` /
103
+ * `include_location` remain on the object so a MemoryLogger can record them
104
+ * on the resulting LogMessage without duplicating them inside `text`.
105
+ * @param {{text: string, source_location?: any, include_location?: any}} obj
100
106
  * @returns {typeof obj} The same object with inspect() and toString() added.
101
107
  */
102
108
  function attach(obj: {
103
109
  text: string;
104
- source_location?: string;
110
+ source_location?: any;
111
+ include_location?: any;
105
112
  }): typeof obj;
106
113
  }
107
114
  }
115
+ /** Wrapper stored by MemoryLogger; provides getSeverity/getText/getSourceLocation. */
116
+ export class LogMessage {
117
+ /**
118
+ * @param {string} severity - Severity label, e.g. 'ERROR'.
119
+ * @param {string|{text: string, source_location?: import('./reader.js').Cursor}|null} message
120
+ */
121
+ constructor(severity: string, message: string | {
122
+ text: string;
123
+ source_location?: import("./reader.js").Cursor;
124
+ } | null);
125
+ message: string | {
126
+ text: string;
127
+ source_location?: import("./reader.js").Cursor;
128
+ };
129
+ /** @type {string} */
130
+ severity: string;
131
+ /** @type {string} */
132
+ text: string;
133
+ /** @type {import('./reader.js').Cursor|null} */
134
+ sourceLocation: import("./reader.js").Cursor | null;
135
+ /**
136
+ * @returns {string} The severity label, e.g. 'ERROR'.
137
+ */
138
+ getSeverity(): string;
139
+ /**
140
+ * @returns {string} The message text.
141
+ */
142
+ getText(): string;
143
+ /**
144
+ * @returns {import('./reader.js').Cursor|undefined} The source location, if any.
145
+ */
146
+ getSourceLocation(): import("./reader.js").Cursor | undefined;
147
+ }
108
148
  /** In-memory logger that stores all log messages for later inspection. */
109
149
  export class MemoryLogger {
110
150
  static create(): MemoryLogger;
111
151
  level: number;
112
- messages: any[];
113
- getMessages(): any[];
152
+ /** @type {LogMessage[]} */
153
+ messages: LogMessage[];
154
+ /**
155
+ * @returns {LogMessage[]} The log messages recorded so far, in order.
156
+ */
157
+ getMessages(): LogMessage[];
114
158
  getMaxSeverity(): number;
115
159
  add(severity: any, message?: any, progname?: any): boolean;
116
160
  debug(msg: any, pn: any): boolean;
package/types/reader.d.ts CHANGED
@@ -123,13 +123,13 @@ export class Reader {
123
123
  createLogMessage(text: any, context?: {}): any;
124
124
  get logger(): any;
125
125
  /** @param {string} msg @param {{ sourceLocation?: any, includeLocation?: any }} [opts] */
126
- _logWarn(msg: string, { sourceLocation, includeLocation }?: {
126
+ _logWarn(msg: string, opts?: {
127
127
  sourceLocation?: any;
128
128
  includeLocation?: any;
129
129
  }): void;
130
130
  _logError(msg: any, opts?: {}): void;
131
131
  /** @param {string} msg @param {{ sourceLocation?: any }} [opts] */
132
- _logInfo(msg: string, { sourceLocation }?: {
132
+ _logInfo(msg: string, opts?: {
133
133
  sourceLocation?: any;
134
134
  }): void;
135
135
  }
@@ -4,9 +4,9 @@ export namespace Substitutors {
4
4
  *
5
5
  * @param {string|string[]} text - The text to process; must not be null.
6
6
  * @param {string[]} [subs=NORMAL_SUBS] - The substitutions to perform.
7
- * @returns {string|string[]} Text with substitutions applied.
7
+ * @returns {Promise<string|string[]>} Text with substitutions applied.
8
8
  */
9
- function applySubs(text: string | string[], subs?: string[]): string | string[];
9
+ function applySubs(text: string | string[], subs?: string[]): Promise<string | string[]>;
10
10
  /** Apply normal substitutions (alias for applySubs with default args). */
11
11
  function applyNormalSubs(text: any): Promise<string | string[]>;
12
12
  /** Apply substitutions for header metadata and attribute assignments.
@@ -32,9 +32,9 @@ export namespace Substitutors {
32
32
  * Substitute quoted text (emphasis, strong, monospaced, etc.)
33
33
  *
34
34
  * @param {string} text
35
- * @returns {string}
35
+ * @returns {Promise<string>}
36
36
  */
37
- function subQuotes(text: string): string;
37
+ function subQuotes(text: string): Promise<string>;
38
38
  /**
39
39
  * Substitute attribute references in the specified text.
40
40
  *
@@ -54,39 +54,39 @@ export namespace Substitutors {
54
54
  * Substitute inline macros (links, images, etc.)
55
55
  *
56
56
  * @param {string} text
57
- * @returns {string}
57
+ * @returns {Promise<string>}
58
58
  */
59
- function subMacros(text: string): string;
59
+ function subMacros(text: string): Promise<string>;
60
60
  /**
61
61
  * Substitute post replacements (hard line breaks).
62
62
  *
63
63
  * @param {string} text
64
- * @returns {string}
64
+ * @returns {Promise<string>}
65
65
  */
66
- function subPostReplacements(text: string): string;
66
+ function subPostReplacements(text: string): Promise<string>;
67
67
  /**
68
68
  * Apply verbatim substitutions on source.
69
69
  *
70
70
  * @param {string} source
71
71
  * @param {boolean} processCallouts
72
- * @returns {string}
72
+ * @returns {Promise<string>}
73
73
  */
74
- function subSource(source: string, processCallouts: boolean): string;
74
+ function subSource(source: string, processCallouts: boolean): Promise<string>;
75
75
  /**
76
76
  * Substitute callout source references.
77
77
  *
78
78
  * @param {string} text
79
- * @returns {string}
79
+ * @returns {Promise<string>}
80
80
  */
81
- function subCallouts(text: string): string;
81
+ function subCallouts(text: string): Promise<string>;
82
82
  /**
83
83
  * Highlight (colorize) the source code using a syntax highlighter.
84
84
  *
85
85
  * @param {string} source
86
86
  * @param {boolean} processCallouts
87
- * @returns {string}
87
+ * @returns {Promise<string>}
88
88
  */
89
- function highlightSource(source: string, processCallouts: boolean): string;
89
+ function highlightSource(source: string, processCallouts: boolean): Promise<string>;
90
90
  /**
91
91
  * Resolve line numbers to highlight from a test string.
92
92
  *
@@ -107,9 +107,9 @@ export namespace Substitutors {
107
107
  * Restore passthrough text by reinserting into placeholder positions.
108
108
  *
109
109
  * @param {string} text
110
- * @returns {string}
110
+ * @returns {Promise<string>}
111
111
  */
112
- function restorePassthroughs(text: string): string;
112
+ function restorePassthroughs(text: string): Promise<string>;
113
113
  /**
114
114
  * Resolve the list of comma-delimited subs against the possible options.
115
115
  *
@@ -143,9 +143,9 @@ export namespace Substitutors {
143
143
  * @param {string} attrlist
144
144
  * @param {string[]} [posattrs=[]]
145
145
  * @param {Object} [opts={}]
146
- * @returns {Object}
146
+ * @returns {Promise<Object>}
147
147
  */
148
- function parseAttributes(attrlist: string, posattrs?: string[], opts?: any): any;
148
+ function parseAttributes(attrlist: string, posattrs?: string[], opts?: any): Promise<any>;
149
149
  function extractAttributesFromText(text: any, defaultText?: any): Promise<any[]>;
150
150
  function extractCallouts(source: any): any[];
151
151
  function restoreCallouts(source: any, calloutMarks: any, sourceOffset?: any): Promise<string>;