diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c9903805a700ef4f7e88b82e6d79f30567884b68..bfaf62623222fdbf1527421b8dd6387383433efa 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,9 +9,14 @@ This project utilizes semantic versioning. === Changed +* *asciidoc-loader*: Upgrade to Asciidoctor.js 2 and allow use of newer patch versions (#522) * *infrastructure*: Migrate Windows CI pipeline from AppVeyor CI to GitLab CI (#732) * *infrastructure*: Run tests nightly on Node 12 and Node 14 (in addition to Node 10) (#731) +=== Fixed + +* *asciidoc-loader*: Don't crash if stem block is empty (#663) + == 3.0.0-alpha.2 (2021-04-08) === Added diff --git a/packages/asciidoc-loader/lib/converter/html5.js b/packages/asciidoc-loader/lib/converter/html5.js index caf9a49e2754a366e1f644e5d2dfa1fde9cf2fe3..a0d132a3179f25d3e2fa4eacdb5a492e502ab749 100644 --- a/packages/asciidoc-loader/lib/converter/html5.js +++ b/packages/asciidoc-loader/lib/converter/html5.js @@ -17,7 +17,7 @@ const Html5Converter = (() => { this[$pageRefCallback] = callbacks.onPageRef this[$imageRefCallback] = callbacks.onImageRef }) - Opal.defn(scope, '$inline_anchor', function convertInlineAnchor (node) { + Opal.defn(scope, '$convert_inline_anchor', function convertInlineAnchor (node) { if (node.getType() === 'xref') { let callback let refSpec = node.getAttribute('path', undefined, false) @@ -41,15 +41,15 @@ const Html5Converter = (() => { node = Opal.module(null, 'Asciidoctor').Inline.$new(node.getParent(), 'anchor', content, options) } } - return Opal.send(this, Opal.find_super_dispatcher(this, 'inline_anchor', convertInlineAnchor), [node]) + return Opal.send(this, Opal.find_super_dispatcher(this, 'convert_inline_anchor', convertInlineAnchor), [node]) }) - Opal.defn(scope, '$image', function convertImage (node) { - return Opal.send(this, Opal.find_super_dispatcher(this, 'image', convertImage), [ + Opal.defn(scope, '$convert_image', function convertImage (node) { + return Opal.send(this, Opal.find_super_dispatcher(this, 'convert_image', convertImage), [ transformImageNode(this, node, node.getAttribute('target')), ]) }) - Opal.defn(scope, '$inline_image', function convertInlineImage (node) { - return Opal.send(this, Opal.find_super_dispatcher(this, 'inline_image', convertInlineImage), [ + Opal.defn(scope, '$convert_inline_image', function convertInlineImage (node) { + return Opal.send(this, Opal.find_super_dispatcher(this, 'convert_inline_image', convertInlineImage), [ transformImageNode(this, node, node.getTarget()), ]) }) diff --git a/packages/asciidoc-loader/lib/include/include-processor.js b/packages/asciidoc-loader/lib/include/include-processor.js index 44ab011101c2e3e3b105bc111e68746cc28742fc..2d35b84c623a5314f2811e514fe9ce8e0d38520c 100644 --- a/packages/asciidoc-loader/lib/include/include-processor.js +++ b/packages/asciidoc-loader/lib/include/include-processor.js @@ -20,10 +20,9 @@ const IncludeProcessor = (() => { }) Opal.defn(scope, '$process', function (doc, reader, target, attrs) { - if (reader.$include_depth() >= Opal.hash_get(reader.maxdepth, 'abs')) { - if (Opal.hash_get(reader.maxdepth, 'abs')) { - log('error', `maximum include depth of ${Opal.hash_get(reader.maxdepth, 'rel')} exceeded`, reader) - } + if (reader.maxdepth === Opal.nil) return + if (reader.$include_depth() >= Opal.hash_get(reader.maxdepth, 'curr')) { + log('error', `maximum include depth of ${Opal.hash_get(reader.maxdepth, 'rel')} exceeded`, reader) return } const resolvedFile = this[$callback](doc, target, reader.$cursor_at_prev_line()) diff --git a/packages/asciidoc-loader/lib/load-asciidoc.js b/packages/asciidoc-loader/lib/load-asciidoc.js index 3ed061555ccbf45b1a818c80b319b0e598a86763..146110ba3beb57a3525bac844cd7f3047b998336 100644 --- a/packages/asciidoc-loader/lib/load-asciidoc.js +++ b/packages/asciidoc-loader/lib/load-asciidoc.js @@ -1,12 +1,12 @@ 'use strict' // IMPORTANT eagerly load Opal to force the String encoding from UTF-16LE to UTF-8 -const Opal = require('opal-runtime').Opal +const Opal = require('asciidoctor-opal-runtime').Opal if ('encoding' in String.prototype && String(String.prototype.encoding) !== 'UTF-8') { String.prototype.encoding = Opal.const_get_local(Opal.const_get_qualified('::', 'Encoding'), 'UTF_8') // eslint-disable-line no-extend-native } -const asciidoctor = require('asciidoctor.js')() +const asciidoctor = require('@asciidoctor/core')() const Extensions = asciidoctor.Extensions const convertImageRef = require('./image/convert-image-ref') const convertPageRef = require('./xref/convert-page-ref') diff --git a/packages/asciidoc-loader/package.json b/packages/asciidoc-loader/package.json index d58735f4e92e74438cb45328ddad01def38373a6..552885c99d78db679d562284d7aa68c529cf7ba9 100644 --- a/packages/asciidoc-loader/package.json +++ b/packages/asciidoc-loader/package.json @@ -17,8 +17,8 @@ }, "main": "lib/index.js", "dependencies": { - "asciidoctor.js": "1.5.9", - "opal-runtime": "1.0.11" + "@asciidoctor/core": "~2.2.0", + "asciidoctor-opal-runtime": "0.3.2" }, "engines": { "node": ">=10.17.0" diff --git a/packages/asciidoc-loader/test/load-asciidoc-test.js b/packages/asciidoc-loader/test/load-asciidoc-test.js index b3cc8f465807cd4e5dd679ee01a9641685451890..53c1fb58875e7ea5dc0ac51852d612d157ea2d08 100644 --- a/packages/asciidoc-loader/test/load-asciidoc-test.js +++ b/packages/asciidoc-loader/test/load-asciidoc-test.js @@ -121,6 +121,20 @@ describe('loadAsciiDoc()', () => { expect(doc.getAttribute('page-layout')).to.eql('home') }) + it('should apply source style to listing block if source-language is set on document', () => { + const contents = heredoc` + :source-language: ruby + + ---- + puts "Hello, World!" + ---- + ` + setInputFileContents(contents) + const doc = loadAsciiDoc(inputFile) + expect(doc.getBlocks()).to.have.lengthOf(1) + expect(doc.getBlocks()[0].getStyle()).to.eql('source') + }) + it('should not hang on mismatched passthrough syntax', () => { const contents = 'Link the system library `+libconfig++.so.9+` located at `+/usr/lib64/libconfig++.so.9+`.' const html = Asciidoctor.convert(contents, { safe: 'safe' }) @@ -225,7 +239,7 @@ describe('loadAsciiDoc()', () => { 'page-relative-src-path': 'page-a.adoc', // computed doctitle: 'Document Title', - docrole: 'docrole', + role: 'docrole', notitle: '', embedded: '', 'safe-mode-name': 'safe', @@ -500,11 +514,9 @@ describe('loadAsciiDoc()', () => { expect(shoutBlockExtension.registered).to.equal(2) expect(html).to.include('RELEASE EARLY. RELEASE OFTEN') - let messages - ;[html, messages] = captureStderr(() => loadAsciiDoc(inputFile).convert()) + html = loadAsciiDoc(inputFile).convert() + expect(shoutBlockExtension.registered).to.equal(2) expect(html).to.include('Release early. Release often.') - expect(messages).to.have.lengthOf(1) - expect(messages[0]).to.include('page-a.adoc: line 2: invalid style for paragraph: shout') }) it('should give extension access to context that includes current file and content catalog', () => { @@ -631,6 +643,50 @@ describe('loadAsciiDoc()', () => { expect(firstBlock.getSourceLines()).to.eql(['before', expectedSource, 'after']) }) + it('should not remove trailing spaces from lines of a non-AsciiDoc include file', () => { + const includeContents = ['puts "Hello"\t', 'sleep 5 ', 'puts "See ya!" '].join('\n') + const contentCatalog = mockContentCatalog({ + family: 'example', + relative: 'visit.rb', + contents: includeContents, + }).spyOn('getById') + setInputFileContents(['----', 'include::example$visit.rb[]', '----'].join('\n')) + const doc = loadAsciiDoc(inputFile, contentCatalog) + expect(contentCatalog.getById) + .nth(1) + .called.with({ + component: 'component-a', + version: 'master', + module: 'module-a', + family: 'example', + relative: 'visit.rb', + }) + expect(doc.getBlocks()).to.have.lengthOf(1) + expect(doc.getBlocks()[0].getContent()).to.equal(includeContents) + }) + + it('should not drop leading and trailing empty lines of AsciiDoc include file', () => { + const includeContents = ['', 'included content', '', ''].join('\n') + const contentCatalog = mockContentCatalog({ + family: 'partial', + relative: 'paragraph.adoc', + contents: includeContents, + }).spyOn('getById') + setInputFileContents(['before', 'include::partial$paragraph.adoc[]', 'after'].join('\n')) + const doc = loadAsciiDoc(inputFile, contentCatalog) + expect(contentCatalog.getById) + .nth(1) + .called.with({ + component: 'component-a', + version: 'master', + module: 'module-a', + family: 'partial', + relative: 'paragraph.adoc', + }) + expect(doc.getBlocks()).to.have.lengthOf(3) + expect(doc.getBlocks()[1].getSourceLines()).to.eql(['included content']) + }) + it('should not crash if contents of included file is undefined', () => { const contentCatalog = mockContentCatalog({ family: 'partial', @@ -1223,7 +1279,7 @@ describe('loadAsciiDoc()', () => { expect(doc.getBlocks()).to.have.lengthOf(1) expect(messages).to.have.lengthOf(1) expect(messages[0].trim()).to.equal( - 'asciidoctor: ERROR: greeting.adoc: line 3: maximum include depth of 1 exceeded' + 'asciidoctor: ERROR: greeting.adoc: line 3: maximum include depth of 0 exceeded' ) }) @@ -1590,6 +1646,28 @@ describe('loadAsciiDoc()', () => { expect(firstBlock.getSourceLines()).to.eql(includeContents.split('\n').filter((l) => l.charAt() !== '#')) }) + it('should not drop leading and trailing empty lines inside a tagged region of AsciiDoc include file', () => { + const includeContents = ['preamble', 'tag::main[]', '', 'included content', '', 'end::main[]', 'trailer'].join('\n') + const contentCatalog = mockContentCatalog({ + family: 'partial', + relative: 'paragraph.adoc', + contents: includeContents, + }).spyOn('getById') + setInputFileContents(['before', 'include::partial$paragraph.adoc[tag=main]', 'after'].join('\n')) + const doc = loadAsciiDoc(inputFile, contentCatalog) + expect(contentCatalog.getById) + .nth(1) + .called.with({ + component: 'component-a', + version: 'master', + module: 'module-a', + family: 'partial', + relative: 'paragraph.adoc', + }) + expect(doc.getBlocks()).to.have.lengthOf(3) + expect(doc.getBlocks()[1].getSourceLines()).to.eql(['included content']) + }) + it('should match tag directives enclosed in circumfix comments', () => { const cssContents = heredoc` /* tag::snippet[] */ @@ -3239,7 +3317,7 @@ describe('loadAsciiDoc()', () => { relative: 'index.adoc', }) ;[ - 'xref:6.5@relnotes::index.adoc[completely removed\\]', + 'xref:6.5@relnotes::index.adoc[completely removed]', '<<6.5@relnotes::index.adoc#,completely removed>>', ].forEach((pageMacro) => { const contents = `Text.footnote:[Support for pixie dust has been ${pageMacro}.]` @@ -3261,7 +3339,7 @@ describe('loadAsciiDoc()', () => { relative: 'index.adoc', }) ;[ - 'xref:6.5@relnotes::index.adoc[completely removed\\]', + 'xref:6.5@relnotes::index.adoc[completely removed]', '<<6.5@relnotes::index.adoc#,completely removed>>', ].forEach((pageMacro) => { const contents = heredoc` diff --git a/packages/cli/lib/cli.js b/packages/cli/lib/cli.js index 172a07e55309292c1fe498faa879c95bb8bded39..e5b5a30156ccb131ee2bee572244f9e54f9d8a83 100644 --- a/packages/cli/lib/cli.js +++ b/packages/cli/lib/cli.js @@ -19,13 +19,15 @@ async function run (argv = process.argv) { } function exitWithError (err, showStack, msg = undefined) { - msg = `error: ${msg || err.message || err}` - // NOTE: the fallback for locating the stack can likely be removed after upgrading to Asciidoctor 2 - console.error( - showStack - ? err.stack || (err.backtrace ? [msg, ...err.backtrace.slice(1)].join('\n') : `${msg} (no stack)`) - : `${msg}\nAdd the --stacktrace option to see the cause.` - ) + if (!msg) msg = err.message || err + if (showStack) { + const stack = err.backtrace || (err.stack && err.stack.split('\n')) || [] + const msgLine = stack.shift() + msg = msgLine && msgLine.endsWith(': ' + msg) ? msgLine : `error: ${msg}` + console.error(stack.length ? [msg, ...stack].join('\n') : `${msg} (no stack)`) + } else { + console.error(`error: ${msg}\nAdd the --stacktrace option to see the cause.`) + } process.exit(1) } diff --git a/packages/cli/test/fixtures/global-fail-tree-processor.js b/packages/cli/test/fixtures/global-fail-tree-processor.js index d536b3f969792caf888e8c822a3dc4e61d4a0297..1d99dac5526442b3d2425ec868a1c5720143d0d7 100644 --- a/packages/cli/test/fixtures/global-fail-tree-processor.js +++ b/packages/cli/test/fixtures/global-fail-tree-processor.js @@ -1,6 +1,6 @@ 'use strict' -const asciidoctor = require('asciidoctor.js')() +const asciidoctor = require('@asciidoctor/core')() asciidoctor.Extensions.register(function () { this.treeProcessor(function () { diff --git a/packages/cli/test/fixtures/global-postprocessor.js b/packages/cli/test/fixtures/global-postprocessor.js index 1afbd32271fc31d4b5d43c0eacc9345c91d96b09..11eea35d7bb44950e447aa4c5ecc5a783a3a7747 100644 --- a/packages/cli/test/fixtures/global-postprocessor.js +++ b/packages/cli/test/fixtures/global-postprocessor.js @@ -1,6 +1,6 @@ 'use strict' -const asciidoctor = require('asciidoctor.js')() +const asciidoctor = require('@asciidoctor/core')() asciidoctor.Extensions.register(function () { this.postprocessor(function () { diff --git a/packages/document-converter/test/convert-document-test.js b/packages/document-converter/test/convert-document-test.js index cab1c0134b48bbc2382bdf5222ef841ceeaf59bd..844c2078cc7ea2013f2b96ed8d3984116c111822 100644 --- a/packages/document-converter/test/convert-document-test.js +++ b/packages/document-converter/test/convert-document-test.js @@ -125,8 +125,7 @@ describe('convertDocument()', () => { `) }) - // TODO: reverse this test once Asciidoctor.js is upgraded - it('should throw an exception if contents of stem block is empty', () => { + it('should not throw exception if contents of stem block is empty', () => { inputFile.contents = Buffer.from(heredoc` = Page Title @@ -134,7 +133,8 @@ describe('convertDocument()', () => { ++++ ++++ `) - expect(() => convertDocument(inputFile, undefined, asciidocConfig)).to.throw("undefined method `include?' for nil") + expect(() => convertDocument(inputFile, undefined, asciidocConfig)).to.not.throw() + expect(inputFile.contents.toString()).to.include('