Migration guide: @asciidoctor/core v3 → v4
@asciidoctor/core v4 is a complete rewrite of the library.
v3 relied on Opal (a Ruby-to-JavaScript transpiler) to produce a JS bundle from the Ruby source.
v4 is a native JavaScript implementation — no transpilation, no Ruby, pure ESM.
The library source is written in ESM only.
For compatibility, a CommonJS bundle is also provided and the package.json exports field points to the appropriate format automatically — require() continues to work.
Breaking changes
Asynchronous API
All parsing and conversion operations now return a Promise and must be awaited.
| v3 (Opal) | v4 (native) | Note |
|---|---|---|
|
|
Must be awaited |
|
|
Must be awaited |
|
|
Must be awaited |
|
|
Must be awaited |
|
|
Must be awaited |
|
|
Must be awaited |
No factory call
v3 exports a factory function that must be called to obtain an Asciidoctor instance. v4 uses named exports — import only what you need, no factory call needed.
| v3 | v4 |
|---|---|
|
|
Strict camelCase naming
Some internal properties and options that retained the Ruby snake_case style in v3 are now camelCase.
v3 (snake_case) |
v4 (camelCase) |
|---|---|
|
|
|
|
|
|
|
|
Renamed methods
| v3 | v4 | Note |
|---|---|---|
|
|
Checks whether the |
|
|
Checks whether the |
|
|
Checks whether the |
|
|
|
getAttribute returns null instead of undefined
In v3, getAttribute(name) returned undefined when the attribute was absent.
In v4, it returns null.
| v3 | v4 |
|---|---|
|
|
Update any code that checks the return value against undefined:
// v3
if (node.getAttribute('language') === undefined) { ... }
// v4
if (node.getAttribute('language') === null) { ... }
// or equivalently
if (!node.getAttribute('language')) { ... }
Restructured inner classes
| v3 | v4 | Note |
|---|---|---|
|
|
Renamed to avoid collision with |
|
|
Top-level export |
|
|
Top-level export |
|
|
Top-level export |
Asynchronous methods on AbstractNode
| v3 | v4 |
|---|---|
|
|
|
|
|
|
|
|
Asynchronous methods on AbstractBlock
| v3 | v4 |
|---|---|
|
|
|
|
|
|
Extensions transpiled with Opal
Extensions written in Ruby and transpiled to JavaScript via Opal will no longer work. v4 does not include the Opal runtime, so any Opal-generated code will fail at load time.
Extensions must be rewritten in pure JavaScript (ESM). The extension API is largely similar, but some differences may exist — refer to the extension documentation for details.
In addition, extension processors can now be asynchronous: the process method may return a Promise, which will be properly awaited during conversion.
| v3 | v4 |
|---|---|
Extensions written in Ruby, transpiled via Opal |
Extensions written in pure JavaScript (ESM) |
Custom converters
In v4, node.getContent() returns a Promise, so any converter that calls it must await the result.
The convert() method itself can remain synchronous if it does not call getContent() or any other async API.
// v4 — synchronous converter (no async content call)
convert(node, transform) {
return node.getTitle()
}
// v4 — async converter when getContent() is needed
async convert(node, transform) {
return `<p>${await node.getContent()}</p>`
}
Custom syntax highlighters
The syntax highlighter API distinguishes between two methods with different async requirements:
-
highlight(node, source, lang, opts)— must remain synchronous, returns astringor[string, number] -
format(node, lang, opts)— can be synchronous or asynchronous; returning aPromise<string>is recommended for I/O-bound operations, but a plainstringworks too
CLI: --require no longer registers extensions
This change applies to the asciidoctor CLI package.
In v3, --require (-r) loaded the given module and automatically called its exported register function, so it was the way to load extensions from the command line.
In v4, these two concerns are separate options:
-
--require <library>(-r) — loads the module for its side effects only, without calling any exported function. Use it for libraries that configure themselves on load (syntax highlighters, polyfills, etc.) or for extensions that self-register at the top level viaExtensions.register(). -
--extension <extension>— loads the module and calls itsregister(registry)named export with a shared registry. Repeat the option to load multiple extensions.
For example, to load the asciidoctor-kroki extension:
$ asciidoctor -r asciidoctor-kroki doc.adoc (1)
$ asciidoctor --extension asciidoctor-kroki doc.adoc (2)
| 1 | v3 |
| 2 | v4 |
New in v4
Named exports
v4 is named-exports only — there is no default export. Every public member is available as a named export.
import { load, convert, loadFile, convertFile, getVersion, getCoreVersion } from '@asciidoctor/core'
import {
Document,
DocumentTitle,
Author,
Footnote,
ImageReference,
RevisionInfo,
AbstractNode,
AbstractBlock,
Block,
Section,
Inline,
List,
ListItem,
} from '@asciidoctor/core'
import { Reader, Cursor } from '@asciidoctor/core'
import {
ConverterFactory,
DefaultConverterFactory,
Html5Converter,
} from '@asciidoctor/core'
import {
Extensions,
Registry,
ProcessorExtension,
Preprocessor,
TreeProcessor,
Postprocessor,
IncludeProcessor,
DocinfoProcessor,
BlockProcessor,
InlineMacroProcessor,
BlockMacroProcessor,
} from '@asciidoctor/core'
import {
SyntaxHighlighter,
SyntaxHighlighterBase,
DefaultSyntaxHighlighterFactory,
} from '@asciidoctor/core'
import {
Logger,
LoggerManager,
MemoryLogger,
NullLogger,
SafeMode,
ContentModel,
Timings,
} from '@asciidoctor/core'
loadFile and convertFile are not available as named exports in the browser build.
|
Document direct properties
The v3 getter methods are preserved unchanged. In addition, v4 exposes these values as direct instance properties for convenience.
| Getter method (v3 and v4) | Direct property (new in v4) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|