Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce an async convert #644

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions benchmark/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const asciidoctor = require('../build/asciidoctor-node.js')()
const runs = process.env.RUNS || 5

const run = async (label, fn) => {
console.time(label)
const resultAsyncPromises = []
for (let i = 0; i < runs; i++) {
resultAsyncPromises.push(fn())
}
const resultAsync = await Promise.all(resultAsyncPromises)
console.log(resultAsync.length)
console.timeEnd(label)
}

const includeBaseDirectory = `${__dirname}/fixtures/includes`
const localIncludeInput = `
include::${includeBaseDirectory}/1.adoc[]
include::${includeBaseDirectory}/2.adoc[]
include::${includeBaseDirectory}/3.adoc[]
include::${includeBaseDirectory}/4.adoc[]
include::${includeBaseDirectory}/5.adoc[]
include::${includeBaseDirectory}/6.adoc[]
include::${includeBaseDirectory}/7.adoc[]
include::${includeBaseDirectory}/8.adoc[]
include::${includeBaseDirectory}/9.adoc[]
include::${includeBaseDirectory}/10.adoc[]
include::${includeBaseDirectory}/11.adoc[]
include::${includeBaseDirectory}/12.adoc[]
include::${includeBaseDirectory}/13.adoc[]
include::${includeBaseDirectory}/14.adoc[]
include::${includeBaseDirectory}/15.adoc[]
include::${includeBaseDirectory}/16.adoc[]
include::${includeBaseDirectory}/17.adoc[]
include::${includeBaseDirectory}/18.adoc[]
include::${includeBaseDirectory}/19.adoc[]
include::${includeBaseDirectory}/20.adoc[]
`

const remoteIncludeInput = `
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor.js/master/README.adoc[]
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor.js/master/README.adoc[]
include::https://raw.githubusercontent.com/asciidoctor/asciidoctor.js/master/README.adoc[]
`

;(async () => {
console.log('warmup...')
for (let i = 0; i < 100; i++) {
const doc = asciidoctor.load(localIncludeInput, { safe: 'safe' })
doc.convert({ safe: 'safe' })
await asciidoctor.convertAsync(localIncludeInput, { safe: 'safe' })
}
await run('(local include) - convert', () => {
const doc = asciidoctor.load(localIncludeInput, { safe: 'safe' })
return doc.convert({ safe: 'safe' })
})
await run('(local include) - convertAsync', async () => await asciidoctor.convertAsync(localIncludeInput, { safe: 'safe' }))
await run('(local include) - convertAsync-Promise.all', () => asciidoctor.convertAsync(localIncludeInput, { safe: 'safe' }))

await run('(remote include) - convert', () => {
const doc = asciidoctor.load(remoteIncludeInput, { safe: 'safe', attributes: { 'allow-uri-read': true } })
return doc.convert({ safe: 'safe' })
})
await run('(remote include) - convertAsync', async () => await asciidoctor.convertAsync(remoteIncludeInput, { safe: 'safe', attributes: { 'allow-uri-read': true } }))
await run('(remote include) - convertAsync-Promise.all', () => asciidoctor.convertAsync(remoteIncludeInput, { safe: 'safe', attributes: { 'allow-uri-read': true } }))
})()
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/1.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/10.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
10
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/11.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
11
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/12.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/13.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
13
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/14.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
14
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/15.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
15
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/16.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
16
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/17.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
17
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/18.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/19.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
19
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/20.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/3.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/4.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/5.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/6.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/7.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/8.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
8
1 change: 1 addition & 0 deletions benchmark/fixtures/includes/9.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9
209 changes: 209 additions & 0 deletions lib/asciidoctor/js/opal_ext/reader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
module Asciidoctor
# Utility methods extracted from the "preprocess_include_directive" method
# https://github.com/asciidoctor/asciidoctor/blob/ebb05a60ff7b4d61655d9d4ec33e5e0100f17de8/lib/asciidoctor/reader.rb#L867
class PreprocessorReader < Reader
# Internal: Preprocess the directive to include lines from another document.
#
# Preprocess the directive to include the target document. The scenarios
# are as follows:
#
# If SafeMode is SECURE or greater, the directive is ignore and the include
# directive line is emitted verbatim.
#
# Otherwise, if an include processor is specified pass the target and
# attributes to that processor and expect an Array of String lines in return.
#
# Otherwise, if the max depth is greater than 0, and is not exceeded by the
# stack size, normalize the target path and read the lines onto the beginning
# of the Array of source data.
#
# If none of the above apply, emit the include directive line verbatim.
#
# target - The unsubstituted String name of the target document to include as specified in the
# target slot of the include directive.
# attrlist - An attribute list String, which is the text between the square brackets of the
# include directive.
#
# Returns a [Boolean] indicating whether the line under the cursor was changed. To skip over the
# directive, call shift and return true.
def preprocess_include_directive target, attrlist
doc = @document
if ((expanded_target = target).include? ATTR_REF_HEAD) &&
(expanded_target = doc.sub_attributes target, :attribute_missing => 'drop-line').empty?
shift
if (doc.attributes['attribute-missing'] || Compliance.attribute_missing) == 'skip'
unshift %(Unresolved directive in #{@path} - include::#{target}[#{attrlist}])
end
true
elsif include_processors? && (ext = @include_processor_extensions.find {|candidate| candidate.instance.handles? expanded_target })
shift
# FIXME parse attributes only if requested by extension
ext.process_method[doc, self, expanded_target, (doc.parse_attributes attrlist, [], :sub_input => true)]
true
# if running in SafeMode::SECURE or greater, don't process this directive
# however, be friendly and at least make it a link to the source document
elsif doc.safe >= SafeMode::SECURE
# FIXME we don't want to use a link macro if we are in a verbatim context
replace_next_line %(link:#{expanded_target}[])
elsif (abs_maxdepth = @maxdepth[:abs]) > 0
if @include_stack.size >= abs_maxdepth
logger.error message_with_context %(maximum include depth of #{@maxdepth[:rel]} exceeded), :source_location => cursor
return
end

parsed_attrs = doc.parse_attributes attrlist, [], :sub_input => true
inc_path, target_type, relpath = resolve_include_path expanded_target, attrlist, parsed_attrs
return inc_path unless target_type

inc_linenos = inc_tags = nil
if attrlist
if parsed_attrs.key? 'lines'
inc_linenos = []
(split_delimited_value parsed_attrs['lines']).each do |linedef|
if linedef.include? '..'
from, to = linedef.split '..', 2
inc_linenos += (to.empty? || (to = to.to_i) < 0) ? [from.to_i, 1.0/0.0] : ::Range.new(from.to_i, to).to_a
else
inc_linenos << linedef.to_i
end
end
inc_linenos = inc_linenos.empty? ? nil : inc_linenos.sort.uniq
elsif parsed_attrs.key? 'tag'
unless (tag = parsed_attrs['tag']).empty? || tag == '!'
inc_tags = (tag.start_with? '!') ? { (tag.slice 1, tag.length) => false } : { tag => true }
end
elsif parsed_attrs.key? 'tags'
inc_tags = {}
(split_delimited_value parsed_attrs['tags']).each do |tagdef|
if tagdef.start_with? '!'
inc_tags[tagdef.slice 1, tagdef.length] = false
else
inc_tags[tagdef] = true
end unless tagdef.empty? || tagdef == '!'
end
inc_tags = nil if inc_tags.empty?
end
end

if inc_linenos
inc_lines, inc_offset, inc_lineno = [], nil, 0
begin
select_remaining = nil
read_include_content(inc_path, target_type).each_line do |l|
inc_lineno += 1
if select_remaining || (::Float === (select = inc_linenos[0]) && (select_remaining = select.infinite?))
# NOTE record line where we started selecting
inc_offset ||= inc_lineno
inc_lines << l
else
if select == inc_lineno
# NOTE record line where we started selecting
inc_offset ||= inc_lineno
inc_lines << l
inc_linenos.shift
end
break if inc_linenos.empty?
end
end
rescue
logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
end
shift
# FIXME not accounting for skipped lines in reader line numbering
if inc_offset
parsed_attrs['partial-option'] = true
push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
end
elsif inc_tags
inc_lines, inc_offset, inc_lineno, tag_stack, tags_used, active_tag = [], nil, 0, [], ::Set.new, nil
if inc_tags.key? '**'
if inc_tags.key? '*'
select = base_select = (inc_tags.delete '**')
wildcard = inc_tags.delete '*'
else
select = base_select = wildcard = (inc_tags.delete '**')
end
else
select = base_select = !(inc_tags.value? true)
wildcard = inc_tags.delete '*'
end
begin
dbl_co, dbl_sb = '::', '[]'
encoding = ::Encoding::UTF_8 if COERCE_ENCODING
read_include_content(inc_path, target_type).each_line do |l|
inc_lineno += 1
# must force encoding since we're performing String operations on line
l.force_encoding encoding if encoding
if (l.include? dbl_co) && (l.include? dbl_sb) && TagDirectiveRx =~ l
if $1 # end tag
if (this_tag = $2) == active_tag
tag_stack.pop
active_tag, select = tag_stack.empty? ? [nil, base_select] : tag_stack[-1]
elsif inc_tags.key? this_tag
include_cursor = create_include_cursor inc_path, expanded_target, inc_lineno
if (idx = tag_stack.rindex {|key, _| key == this_tag})
idx == 0 ? tag_stack.shift : (tag_stack.delete_at idx)
logger.warn message_with_context %(mismatched end tag (expected '#{active_tag}' but found '#{this_tag}') at line #{inc_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => include_cursor
else
logger.warn message_with_context %(unexpected end tag '#{this_tag}' at line #{inc_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => include_cursor
end
end
elsif inc_tags.key?(this_tag = $2)
tags_used << this_tag
# QUESTION should we prevent tag from being selected when enclosing tag is excluded?
tag_stack << [(active_tag = this_tag), (select = inc_tags[this_tag]), inc_lineno]
elsif !wildcard.nil?
select = active_tag && !select ? false : wildcard
tag_stack << [(active_tag = this_tag), select, inc_lineno]
end
elsif select
# NOTE record the line where we started selecting
inc_offset ||= inc_lineno
inc_lines << l
end
end
rescue
logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
end
unless tag_stack.empty?
tag_stack.each do |tag_name, _, tag_lineno|
logger.warn message_with_context %(detected unclosed tag '#{tag_name}' starting at line #{tag_lineno} of include #{target_type}: #{inc_path}), :source_location => cursor, :include_location => (create_include_cursor inc_path, expanded_target, tag_lineno)
end
end
unless (missing_tags = inc_tags.keys.to_a - tags_used.to_a).empty?
logger.warn message_with_context %(tag#{missing_tags.size > 1 ? 's' : ''} '#{missing_tags.join ', '}' not found in include #{target_type}: #{inc_path}), :source_location => cursor
end
shift
if inc_offset
parsed_attrs['partial-option'] = true unless base_select && wildcard && inc_tags.empty?
# FIXME not accounting for skipped lines in reader line numbering
push_include inc_lines, inc_path, relpath, inc_offset, parsed_attrs
end
else
begin
# NOTE read content first so that we only advance cursor if IO operation succeeds
inc_content = read_include_content inc_path, target_type
shift
push_include inc_content, inc_path, relpath, 1, parsed_attrs
rescue
logger.error message_with_context %(include #{target_type} not readable: #{inc_path}), :source_location => cursor
return replace_next_line %(Unresolved directive in #{@path} - include::#{expanded_target}[#{attrlist}])
end
end
true
end
end

# If a VFS is defined, Asciidoctor will use it to resolve the include target.
# Otherwise use the file system or the network to read the file.
def read_include_content inc_path, target_type
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mojavelinux preprocess_include_directive method is almost identical, I've just replaced File.open with the new read_include_content method.

if (vfs = @document.options['vfs']) && (content = vfs[inc_path])
content
else
target_type == :file ? ::File.open(inc_path, 'rb') {|f| f.read } : open(inc_path, 'rb') {|f| f.read }
end
end
end
end
1 change: 1 addition & 0 deletions lib/asciidoctor/js/postscript.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'asciidoctor/converter/composite'
require 'asciidoctor/converter/html5'
require 'asciidoctor/extensions'
require 'asciidoctor/js/opal_ext/reader'

if JAVASCRIPT_IO_MODULE == 'xmlhttprequest'
require 'asciidoctor/js/opal_ext/browser/reader'
Expand Down
1 change: 1 addition & 0 deletions spec/fixtures/includes/1.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions spec/fixtures/includes/2.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2
62 changes: 62 additions & 0 deletions spec/node/asciidoctor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,68 @@ In other words, it’s about discovering writing zen.`
})
})

describe('Async convert', () => {
it('should resolve the inline conditional include if the condition is true', async () => {
const input = `:include1:
ifdef::include1[include::spec/fixtures/includes/1.adoc[]]
include::spec/fixtures/includes/2.adoc[]
`
expect(await asciidoctor.convertAsync(input, { safe: 'safe' })).to.contain(`<p>1
2</p>`)
})

it('should not throw an exception if the target is not readable', async () => {
expect(async () => asciidoctor.convertAsync('include::404.adoc[]', { safe: 'safe' })).to.not.throw()
})

it('should resolve the conditional include if the condition is true', async () => {
const input = `:include1:
ifdef::include1[]
include::spec/fixtures/includes/1.adoc[]
endif::[]
include::spec/fixtures/includes/2.adoc[]
`
expect(await asciidoctor.convertAsync(input, { safe: 'safe' })).to.contain(`<p>1
2</p>`)
})

it('should not resolve the inline conditional include if the condition is false', async () => {
const input = `
ifdef::include1[include::spec/fixtures/includes/1.adoc[]]
include::spec/fixtures/includes/2.adoc[]
`
expect(await asciidoctor.convertAsync(input, { safe: 'safe' })).to.contain('<p>2</p>')
})

it('should not resolve the conditional include if the condition is false', async () => {
const input = `
ifdef::include1[]
include::spec/fixtures/includes/1.adoc[]
endif::[]
include::spec/fixtures/includes/2.adoc[]
`
expect(await asciidoctor.convertAsync(input, { safe: 'safe' })).to.contain('<p>2</p>')
})

it('should ignore escaped include directive', async () => {
const input = `
\\include::spec/fixtures/includes/1.adoc[]
`
const result = await asciidoctor.convertAsync(input, { safe: 'safe' })
expect(result).to.contain('include::spec/fixtures/includes/1.adoc[]')
})

it('should ignore comment block', async () => {
const input = `
////
include::spec/fixtures/includes/1.adoc[]
include::spec/fixtures/includes/2.adoc[]
////
`
expect(await asciidoctor.convertAsync(input, { safe: 'safe' })).to.contain('')
})
})

if (isWin && process.env.APPVEYOR_BUILD_FOLDER) {
describe('Windows', () => {
it('should register a custom converter', () => {
Expand Down
Loading