Method: Asciidoctor::Document#initialize

Defined in:
lib/asciidoctor/document.rb

#initialize(data = nil, options = {}) ⇒ Document

Initialize a Asciidoctor::Document object.

Examples:

data = File.read filename
doc = Asciidoctor::Document.new data
puts doc.convert

Parameters:

  • data (defaults to: nil)

    The AsciiDoc source data as a String or String Array. (default: nil)

  • options (defaults to: {})

    A Hash of options to control processing (e.g., safe mode value (:safe), backend (:backend), standalone enclosure (:standalone), custom attributes (:attributes)). (default: {})

  • Duplication

    of the options Hash is handled in the enclosing API.



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/asciidoctor/document.rb', line 253

def initialize data = nil, options = {}
  super self, :document

  if (parent_doc = options.delete :parent)
    @parent_document = parent_doc
    options[:base_dir] ||= parent_doc.base_dir
    options[:catalog_assets] = true if parent_doc.options[:catalog_assets]
    options[:to_dir] = parent_doc.options[:to_dir] if parent_doc.options[:to_dir]
    @catalog = parent_doc.catalog.merge footnotes: []
    # QUESTION should we support setting attribute in parent document from nested document?
    @attribute_overrides = attr_overrides = (parent_doc.instance_variable_get :@attribute_overrides).merge parent_doc.attributes
    attr_overrides.delete 'compat-mode'
    parent_doctype = attr_overrides.delete 'doctype'
    attr_overrides.delete 'notitle'
    attr_overrides.delete 'showtitle'
    # QUESTION if toc is hard unset in parent document, should it be hard unset in nested document?
    attr_overrides.delete 'toc'
    @attributes['toc-placement'] = (attr_overrides.delete 'toc-placement') || 'auto'
    attr_overrides.delete 'toc-position'
    @safe = parent_doc.safe
    @attributes['compat-mode'] = '' if (@compat_mode = parent_doc.compat_mode)
    @outfilesuffix = parent_doc.outfilesuffix
    @sourcemap = parent_doc.sourcemap
    @timings = nil
    @path_resolver = parent_doc.path_resolver
    @converter = parent_doc.converter
    initialize_extensions = nil
    @extensions = parent_doc.extensions
    @syntax_highlighter = parent_doc.syntax_highlighter
  else
    @parent_document = nil
    @catalog = {
      ids: {}, # deprecated; kept for backwards compatibility with converters
      refs: {},
      footnotes: [],
      links: [],
      images: [],
      callouts: Callouts.new,
      includes: {},
    }
    # copy attributes map and normalize keys
    # attribute overrides are attributes that can only be set from the commandline
    # a direct assignment effectively makes the attribute a constant
    # a nil value or name with leading or trailing ! will result in the attribute being unassigned
    @attribute_overrides = attr_overrides = {}
    (options[:attributes] || {}).each do |key, val|
      if key.end_with? '@'
        if key.start_with? '!'
          key, val = (key.slice 1, key.length - 2), false
        elsif key.end_with? '!@'
          key, val = (key.slice 0, key.length - 2), false
        else
          key, val = key.chop, %(#{val}@)
        end
      elsif key.start_with? '!'
        key, val = (key.slice 1, key.length), val == '@' ? false : nil
      elsif key.end_with? '!'
        key, val = key.chop, val == '@' ? false : nil
      end
      attr_overrides[key.downcase] = val
    end
    if ::String === (to_file = options[:to_file])
      attr_overrides['outfilesuffix'] = Helpers.extname to_file
    end
    # safely resolve the safe mode from const, int or string
    if !(safe_mode = options[:safe])
      @safe = SafeMode::SECURE
    elsif ::Integer === safe_mode
      # be permissive in case API user wants to define new levels
      @safe = safe_mode
    else
      @safe = (SafeMode.value_for_name safe_mode) rescue SafeMode::SECURE
    end
    input_mtime = options.delete :input_mtime
    @compat_mode = attr_overrides.key? 'compat-mode'
    @sourcemap = options[:sourcemap]
    @timings = options.delete :timings
    @path_resolver = PathResolver.new
    initialize_extensions = (defined? ::Asciidoctor::Extensions) || (options.key? :extensions) ? ::Asciidoctor::Extensions : nil
    @extensions = nil # initialize further down if initialize_extensions is true
    options[:standalone] = options[:header_footer] if (options.key? :header_footer) && !(options.key? :standalone)
  end

  @parsed = @reftexts = @header = @header_attributes = nil
  @counters = {}
  @attributes_modified = ::Set.new
  @docinfo_processor_extensions = {}
  standalone = options[:standalone]
  (@options = options).freeze

  attrs = @attributes
  unless parent_doc
    attrs['attribute-undefined'] = Compliance.attribute_undefined
    attrs['attribute-missing'] = Compliance.attribute_missing
    attrs.update DEFAULT_ATTRIBUTES
    # TODO if lang attribute is set, @safe mode < SafeMode::SERVER, and !parent_doc,
    # load attributes from data/locale/attributes-<lang>.adoc
  end

  if standalone
    # sync embedded attribute with :standalone option value
    attr_overrides['embedded'] = nil
    attrs['copycss'] = ''
    attrs['iconfont-remote'] = ''
    attrs['stylesheet'] = ''
    attrs['webfonts'] = ''
  else
    # sync embedded attribute with :standalone option value
    attr_overrides['embedded'] = ''
    if (attr_overrides.key? 'showtitle') && (attr_overrides.keys & %w(notitle showtitle))[-1] == 'showtitle'
      attr_overrides['notitle'] = { nil => '', false => '@', '@' => false }[attr_overrides['showtitle']]
    elsif attr_overrides.key? 'notitle'
      attr_overrides['showtitle'] = { nil => '', false => '@', '@' => false }[attr_overrides['notitle']]
    else
      attrs['notitle'] = ''
    end
  end

  attr_overrides['asciidoctor'] = ''
  attr_overrides['asciidoctor-version'] = ::Asciidoctor::VERSION

  attr_overrides['safe-mode-name'] = (safe_mode_name = SafeMode.name_for_value @safe)
  attr_overrides[%(safe-mode-#{safe_mode_name})] = ''
  attr_overrides['safe-mode-level'] = @safe

  # the only way to set the max-include-depth attribute is via the API; default to 64 like AsciiDoc.py
  attr_overrides['max-include-depth'] ||= 64

  # the only way to set the allow-uri-read attribute is via the API; disabled by default
  attr_overrides['allow-uri-read'] ||= nil

  # remap legacy attribute names
  attr_overrides['sectnums'] = attr_overrides.delete 'numbered' if attr_overrides.key? 'numbered'
  attr_overrides['hardbreaks-option'] = attr_overrides.delete 'hardbreaks' if attr_overrides.key? 'hardbreaks'

  # If the base_dir option is specified, it overrides docdir and is used as the root for relative
  # paths. Otherwise, the base_dir is the directory of the source file (docdir), if set, otherwise
  # the current directory.
  if (base_dir_val = options[:base_dir])
    @base_dir = (attr_overrides['docdir'] = ::File.expand_path base_dir_val)
  elsif attr_overrides['docdir']
    @base_dir = attr_overrides['docdir']
  else
    #logger.warn 'setting base_dir is recommended when working with string documents' unless nested?
    @base_dir = attr_overrides['docdir'] = ::Dir.pwd
  end

  # allow common attributes backend and doctype to be set using options hash, coerce values to string
  if (backend_val = options[:backend])
    attr_overrides['backend'] = backend_val.to_s
  end

  if (doctype_val = options[:doctype])
    attr_overrides['doctype'] = doctype_val.to_s
  end

  if @safe >= SafeMode::SERVER
    # restrict document from setting copycss, source-highlighter and backend
    attr_overrides['copycss'] ||= nil
    attr_overrides['source-highlighter'] ||= nil
    attr_overrides['backend'] ||= DEFAULT_BACKEND
    # restrict document from seeing the docdir and trim docfile to relative path
    if !parent_doc && attr_overrides.key?('docfile')
      attr_overrides['docfile'] = attr_overrides['docfile'][(attr_overrides['docdir'].length + 1)..-1]
    end
    attr_overrides['docdir'] = ''
    attr_overrides['user-home'] ||= '.'
    if @safe >= SafeMode::SECURE
      attr_overrides['max-attribute-value-size'] = 4096 unless attr_overrides.key? 'max-attribute-value-size'
      # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API
      #attr_overrides['linkcss'] = (attr_overrides.fetch 'linkcss', '') || nil
      attr_overrides['linkcss'] = '' unless attr_overrides.key? 'linkcss'
      # restrict document from enabling icons
      attr_overrides['icons'] ||= nil
    end
  else
    attr_overrides['user-home'] ||= USER_HOME
  end

  # the only way to set the max-attribute-value-size attribute is via the API; disabled by default
  @max_attribute_value_size = (size = (attr_overrides['max-attribute-value-size'] ||= nil)) ? size.to_i.abs : nil

  attr_overrides.delete_if do |key, val|
    if val
      # a value ending in @ allows document to override value
      if ::String === val && (val.end_with? '@')
        val, verdict = val.chop, true
      end
      attrs[key] = val
    else
      # a nil or false value both unset the attribute; only a nil value locks it
      attrs.delete key
      verdict = val == false
    end
    verdict
  end

  if parent_doc
    @backend = attrs['backend']
    # reset doctype unless it matches the default value
    unless (@doctype = attrs['doctype'] = parent_doctype) == DEFAULT_DOCTYPE
      update_doctype_attributes DEFAULT_DOCTYPE
    end

    # don't need to do the extra processing within our own document
    # FIXME line info isn't reported correctly within include files in nested document
    @reader = Reader.new data, options[:cursor]
    @source_location = @reader.cursor if @sourcemap

    # Now parse the lines in the reader into blocks
    # Eagerly parse (for now) since a subdocument is not a publicly accessible object
    Parser.parse @reader, self

    # should we call some sort of post-parse function?
    restore_attributes
    @parsed = true
  else
    # setup default backend and doctype
    @backend = nil
    if (initial_backend = attrs['backend'] || DEFAULT_BACKEND) == 'manpage'
      @doctype = attrs['doctype'] = attr_overrides['doctype'] = 'manpage'
    else
      @doctype = (attrs['doctype'] ||= DEFAULT_DOCTYPE)
    end
    update_backend_attributes initial_backend, true

    # dynamic intrinstic attribute values

    #attrs['indir'] = attrs['docdir']
    #attrs['infile'] = attrs['docfile']

    # fallback directories
    attrs['stylesdir'] ||= '.'
    attrs['iconsdir'] ||= %(#{attrs.fetch 'imagesdir', './images'}/icons)

    fill_datetime_attributes attrs, input_mtime

    if initialize_extensions
      if (ext_registry = options[:extension_registry])
        # QUESTION should we warn if the value type of this option is not a registry
        if Extensions::Registry === ext_registry || ((defined? ::AsciidoctorJ::Extensions::ExtensionRegistry) &&
            ::AsciidoctorJ::Extensions::ExtensionRegistry === ext_registry)
          @extensions = ext_registry.activate self
        end
      elsif (ext_block = options[:extensions]).nil?
        @extensions = Extensions::Registry.new.activate self unless Extensions.groups.empty?
      elsif ::Proc === ext_block
        @extensions = Extensions.create(&ext_block).activate self
      end
    end

    @reader = PreprocessorReader.new self, data, (Reader::Cursor.new attrs['docfile'], @base_dir), normalize: true
    @source_location = @reader.cursor if @sourcemap
  end
end