public class Attributes {

    private static final char ATTRIBUTE_SEPARATOR = '=';
    private static Format DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static Format TIME_FORMAT = new SimpleDateFormat("HH:mm:ss z");

    public static final String TOC = "toc";
    public static final String TOC_POSITION = "toc-position";
    public static final String TOC_LEVELS = "toclevels";
    public static final String TOC_2 = "toc2";
    public static final String BACKEND = Options.BACKEND;
    public static final String TITLE = "title";
    public static final String DOCTYPE = Options.DOCTYPE;
    public static final String IMAGESDIR = "imagesdir";
    public static final String SOURCE_HIGHLIGHTER = "source-highlighter";
    public static final String SOURCE_LANGUAGE = "source-language";
    public static final String LOCALDATE = "localdate";
    public static final String LOCALTIME = "localtime";
    public static final String DOCDATE = "docdate";
    public static final String DOCTIME = "doctime";
    public static final String STYLESHEET_NAME = "stylesheet";
    public static final String STYLES_DIR = "stylesdir";
    public static final String NOT_STYLESHEET_NAME = STYLESHEET_NAME + "!";
    public static final String LINK_CSS = "linkcss";
    public static final String COPY_CSS = "copycss";
    public static final String ICONS = "icons";
    public static final String ICONFONT_REMOTE = "iconfont-remote";
    public static final String ICONFONT_CDN = "iconfont-cdn";
    public static final String ICONFONT_NAME = "iconfont-name";
    public static final String ICONS_DIR = "iconsdir";
    public static final String DATA_URI = "data-uri";
    public static final String SECTION_NUMBERS = "numbered";
    public static final String IMAGE_ICONS = "";
    public static final String FONT_ICONS = "font";
    public static final String LINK_ATTRS = "linkattrs";
    public static final String EXPERIMENTAL = "experimental";
    public static final String SHOW_TITLE = "showtitle";
    public static final String NOTITLE = "notitle";
    public static final String ALLOW_URI_READ = "allow-uri-read";
    public static final String IGNORE_UNDEFINED = "ignore-undefined";
    public static final String UNTITLED_LABEL = "untitled-label";
    public static final String SET_ANCHORS = "sectanchors";
    public static final String SKIP_FRONT_MATTER = "skip-front-matter";
    public static final String MAX_INCLUDE_DEPTH = "max-include-depth";
    public static final String ATTRIBUTE_MISSING = "attribute-missing";
    public static final String ATTRIBUTE_UNDEFINED = "attribute-undefined";
    public static final String NO_FOOTER = "nofooter";
    public static final String HARDBREAKS = "hardbreaks";
    public static final String SECT_NUM_LEVELS = "sectnumlevels";
    public static final String CACHE_URI = "cache-uri";
    public static final String MATH = "stem";
    public static final String APPENDIX_CAPTION = "appendix-caption";
    public static final String HIDE_URI_SCHEME = "hide-uri-scheme";
    public static final String COMPAT_MODE = "compat-mode";

    private Map<String, Object> attributes = new LinkedHashMap<>();

    /**
     * @deprecated Use {@link #builder} instead.
     */
    @Deprecated
    public Attributes() {
        super();
    }
    
    /**
     * @deprecated Use {@link #builder} instead.
     */
    @Deprecated
    public Attributes(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    /**
     * @deprecated Use {@link #builder} instead.
     */
    @Deprecated
    public Attributes(String[] attributes) {
        this.setAttributes(attributes);
    }

    /**
     * @deprecated Use {@link #builder} instead.
     */
    @Deprecated
    public Attributes(String attributes) {
        this.setAttributes(attributes);
    }

    /**
     * @return Empty AttributesBuilder instance.
     */
    public static AttributesBuilder builder() {
        return new AttributesBuilder();
    }

    /**
     * Allow Asciidoctor to read content from an URI.
     * Additionally the option {@link SafeMode} must be less than {@link SafeMode#SECURE} to enable inclusion of content from an URI.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#include-content-from-a-uri">Asciidoctor User Manual</a>
     * @param allowUriRead {@code true} to enable inclusion of content from an URI
     */
    public void setAllowUriRead(boolean allowUriRead) {
        this.attributes.put(ALLOW_URI_READ, toAsciidoctorFlag(allowUriRead));
    }

    /**
     * Define how to handle missing attribute references.
     * Possible values are:
     * <table>
     *     <tr>
     *         <td>{@code skip}</td>
     *         <td>leave the reference in place (default setting)</td>
     *     </tr>
     *     <tr>
     *         <td>{@code drop}</td>
     *         <td>drop the reference, but not the line</td>
     *     </tr>
     *     <tr>
     *         <td>{@code drop-line}</td>
     *         <td>drop the line on which the reference occurs (compliant behavior)</td>
     *     </tr>
     *     <tr>
     *         <td>{@code warn}</td>
     *         <td>print a warning about the missing attribute</td>
     *     </tr>
     * </table>
     * @see <a href="http://asciidoctor.org/docs/user-manual/#catch-a-missing-or-undefined-attribute">Asciidoctor User Manual</a>
     * @param attributeMissing One of the constants shown in the table above.
     */
    public void setAttributeMissing(String attributeMissing) {
        this.attributes.put(ATTRIBUTE_MISSING, attributeMissing);
    }

    /**
     * Define how to handle expressions that undefine an attribute.
     * Possible values are:
     * <table>
     *     <tr>
     *         <td>{@code drop}</td>
     *         <td>substitute the expression with an empty string after processing it</td>
     *     </tr>
     *     <tr>
     *         <td>{@code drop-line}</td>
     *         <td>drop the line that contains this expression (default setting and compliant behavior)</td>
     *     </tr>
     * </table>
     * @see <a href="http://asciidoctor.org/docs/user-manual/#catch-a-missing-or-undefined-attribute">Asciidoctor User Manual</a>
     * @param attributeUndefined One of the constants shown in the table above.
     */
    public void setAttributeUndefined(String attributeUndefined) {
        this.attributes.put(ATTRIBUTE_UNDEFINED, attributeUndefined);
    }

    /**
     * Sets the backend attribute.
     * @param backend The name of the backend, e.g. {@code docbook}.
     */
    public void setBackend(String backend) {
        this.attributes.put(BACKEND, backend);
    }

    public void setTitle(String title) {
        this.attributes.put(TITLE, title);
    }

    /**
     * Sets the document type, which defines how a document and what parts are rendered.
     * Possible values are:
     * <ul>
     *     <li>{@code article}</li>
     *     <li>{@code book}</li>
     *     <li>{@code inline}</li>
     *     <li>{@code manpage}</li>
     * </ul>
     * @see <a href="http://asciidoctor.org/docs/user-manual/#document-types">Asciidoctor User Manual</a>
     * @param docType One of the constants mentioned above.
     */
    public void setDocType(String docType) {
        this.attributes.put(DOCTYPE, docType);
    }

    /**
     * Sets the directory to which images are resolved if the image target is a relative path.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#set-the-images-directory">Asciidoctor User Manual</a>
     * @param imagesDir The name of the directory.
     */
    public void setImagesDir(String imagesDir) {
        this.attributes.put(IMAGESDIR, imagesDir);
    }

    /**
     * Globally sets the source language attribute when rendering source blocks.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#source-code-blocks">Asciidoctor User Manual</a>
     * @param sourceLanguage The default source language to use, e.g. {@code Java}.
     */
    public void setSourceLanguage(String sourceLanguage) {
        this.attributes.put(SOURCE_LANGUAGE, sourceLanguage);
    }

    /**
     * Sets the source highlighter to use for rendering source blocks.
     * Possible values are:
     * <ul>
     *     <li>coderay</li>
     *     <li>highlightjs</li>
     *     <li>prettify</li>
     * </ul>
     * @see <a href="http://asciidoctor.org/docs/user-manual/#source-code-blocks">Asciidoctor User Manual</a>
     * @param sourceHighlighter One of the constants mentioned above.
     */
    public void setSourceHighlighter(String sourceHighlighter) {
        this.attributes.put(SOURCE_HIGHLIGHTER, sourceHighlighter);
    }

    /**
     * Defines how many documents can be recursively included.
     * @see <a href="https://github.com/asciidoctor/asciidoctor/issues/581">Track include depth and enforce maximum depth #581</a>
     * @param maxIncludeDepth A positive integer.
     */
    public void setMaxIncludeDepth(int maxIncludeDepth) {
        this.attributes.put(MAX_INCLUDE_DEPTH, maxIncludeDepth);
    }

    /**
     * Sets the depth of section numbering.
     * That is if set to 1 only the top level section will be assigned a number.
     * Default is 3.
     * @param sectNumLevels A positive integer.
     */
    public void setSectNumLevels(int sectNumLevels) {
        this.attributes.put(SECT_NUM_LEVELS, sectNumLevels);
    }

    /**
     * Enables or disables preserving of line breaks in a paragraph.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#line-breaks">Asciidoctor User Manual</a>
     * @param hardbreaks {@code true} to enable preserving of line breaks in paragraphs
     */
    public void setHardbreaks(boolean hardbreaks) {
        this.attributes.put(HARDBREAKS, toAsciidoctorFlag(hardbreaks));
    }

    /**
     * Enables or disables caching of content read from URIs
     * @see <a href="http://asciidoctor.org/docs/user-manual/#include-content-from-a-uri">Asciidoctor User Manual</a>
     * @param cacheUri {@code true} to enable caching of content read from URIs
     */
    public void setCacheUri(boolean cacheUri) {
        this.attributes.put(CACHE_URI, toAsciidoctorFlag(cacheUri));
    }

    /**
     * Enables or disables rendering of the URI scheme when rendering URLs.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#url">Asciidoctor User Manual</a>
     * @param hideUriScheme
     */
    public void setHideUriScheme(boolean hideUriScheme) {
        this.attributes.put(HIDE_URI_SCHEME, toAsciidoctorFlag(hideUriScheme));
    }

    /**
     * Defines the prefix added to appendix sections.
     * The default value is {@code Appendix}
     * @see <a href="http://asciidoctor.org/docs/user-manual/#user-appendix">Asciidoctor User Manual</a>
     * @param appendixCaption The string that is prefixed to the section name in the appendix.
     */
    public void setAppendixCaption(String appendixCaption) {
        this.attributes.put(APPENDIX_CAPTION, appendixCaption);
    }


    /**
     * Sets the interpreter to use for rendering stems, i.e. equations and formulas.
     * @see <a href="http://asciidoctor.org/docs/user-manual/#activating-stem-support">Asciidoctor User Manual</a>
     * @param math The name of the interpreter, i.e. either {@code asciimath} or {@code latexmath}.
     */
    public void setMath(String math) {
        this.attributes.put(MATH, math);
    }

    /**
     * Skips front matter.
     * 
     * @param skipFrontMatter
     *            value.
     */
    public void setSkipFrontMatter(boolean skipFrontMatter) {
        this.attributes.put(SKIP_FRONT_MATTER, toAsciidoctorFlag(skipFrontMatter));
    }

    /**
     * Sets setanchor flag.
     * 
     * @param setAnchors
     *            value.
     */
    public void setAnchors(boolean setAnchors) {
        this.attributes.put(SET_ANCHORS, setAnchors);
    }

    /**
     * Sets the untitled label value.
     * 
     * @param untitledLabel
     *            value.
     */
    public void setUntitledLabel(String untitledLabel) {
        this.attributes.put(UNTITLED_LABEL, untitledLabel);
    }

    /**
     * Sets ignore undefined flag so lines are kept when they contain a reference to a missing attribute.
     * 
     * @param ignoreUndefinedAttributes
     *            value.
     */
    public void setIgnoreUndefinedAttributes(boolean ignoreUndefinedAttributes) {
        this.attributes.put(IGNORE_UNDEFINED, toAsciidoctorFlag(ignoreUndefinedAttributes));
    }

    /**
     * Sets table of contents 2 attribute.
     * 
     * @param placement
     *            where toc is rendered.
     * @deprecated Use {@link #setTableOfContents(Placement)}
     */
    @Deprecated
    public void setTableOfContents2(Placement placement) {
        this.attributes.put(TOC_2, toAsciidoctorFlag(true));
        this.attributes.put(TOC_POSITION, placement.getPosition());
    }

    /**
     * Sets if a table of contents should be rendered or not.
     * 
     * @param placement
     *            position of toc.
     */
    public void setTableOfContents(Placement placement) {
        this.attributes.put(TOC, placement.getPosition());
    }

    /**
     * Sets the amount of levels which should be shown in the toc.
     * 
     * @param levels
     *            number of levels which should be shown in the toc.
     */
    public void setTocLevels(int levels) {
        this.attributes.put(TOC_LEVELS, levels);
    }

    /**
     * Sets showtitle value as an alias for notitle!
     * 
     * @param showTitle
     *            value.
     */
    public void setShowTitle(boolean showTitle) {
        if (showTitle) {
            this.attributes.put(SHOW_TITLE, true);
            this.attributes.remove(NOTITLE);
        } else {
            this.attributes.put(NOTITLE, true);
            this.attributes.remove(SHOW_TITLE);
        }
    }

    /**
     * Sets if a table of contents should be rendered or not.
     * 
     * @param toc
     *            value.
     */
    public void setTableOfContents(boolean toc) {
        if (toc) {
            this.attributes.put(TOC, "");
        } else {
            this.attributes.remove(TOC);
        }
    }

    /**
     * Sets date in format yyyy-MM-dd
     * 
     * @param localDate
     *            object.
     */
    public void setLocalDate(Date localDate) {
        this.attributes.put(LOCALDATE, toDate(localDate));
    }

    /**
     * Sets time in format HH:mm:ss z
     * 
     * @param localTime
     *            object.
     */
    public void setLocalTime(Date localTime) {
        this.attributes.put(LOCALTIME, toTime(localTime));
    }

    /**
     * Sets date in format yyyy-MM-dd
     * 
     * @param docDate
     *            object.
     */
    public void setDocDate(Date docDate) {
        this.attributes.put(DOCDATE, toDate(docDate));
    }

    /**
     * Sets time in format HH:mm:ss z
     * 
     * @param docTime
     *            object.
     */
    public void setDocTime(Date docTime) {
        this.attributes.put(DOCTIME, toTime(docTime));
    }

    /**
     * Sets stylesheet name.
     * 
     * @param styleSheetName
     *            of css file.
     */
    public void setStyleSheetName(String styleSheetName) {
        this.attributes.put(STYLESHEET_NAME, styleSheetName);
    }

    /**
     * Unsets stylesheet name so document will be generated without style.
     */
    public void unsetStyleSheet() {
        this.attributes.put(NOT_STYLESHEET_NAME, toAsciidoctorFlag(true));
    }

    /**
     * Sets the styles dir.
     * 
     * @param stylesDir
     *            directory.
     */
    public void setStylesDir(String stylesDir) {
        this.attributes.put(STYLES_DIR, stylesDir);
    }

    /**
     * Sets link css attribute.
     * 
     * @param linkCss
     *            true if css is linked, false if css is embedded.
     */
    public void setLinkCss(boolean linkCss) {
        this.attributes.put(LINK_CSS, toAsciidoctorFlag(linkCss));
    }

    /**
     * Sets copy css attribute.
     * 
     * @param copyCss
     *            true if css should be copied to the output location, false otherwise.
     */
    public void setCopyCss(boolean copyCss) {
        this.attributes.put(COPY_CSS, toAsciidoctorFlag(copyCss));
    }

    /**
     * Sets which admonition icons to use. Attributes.IMAGE_ICONS constant can be used to use the original icons with
     * images or Attributes.FONT_ICONS for font icons (font-awesome).
     * 
     * @param iconsName
     *            value.
     */
    public void setIcons(String iconsName) {
        this.attributes.put(ICONS, iconsName);
    }

    /**
     * Enable icon font remote attribute. If enabled, will use the iconfont-cdn value to load the icon font URI; if
     * disabled, will use the iconfont-name value to locate the icon font CSS file
     * 
     * @param iconFontRemote
     *            true if attribute enabled false otherwise.
     */
    public void setIconFontRemote(boolean iconFontRemote) {
        this.attributes.put(ICONFONT_REMOTE, toAsciidoctorFlag(iconFontRemote));
    }

    /**
     * The URI prefix of the icon font; looks for minified CSS file based on iconfont-name value; used when
     * iconfont-remote is set
     * 
     * @param cdnUri
     *            uri where css is stored.
     */
    public void setIconFontCdn(URI cdnUri) {
        this.attributes.put(ICONFONT_CDN, cdnUri.toASCIIString());
    }

    /**
     * The name of the stylesheet in the stylesdir to load (.css extension added automatically)
     * 
     * @param iconFontName
     *            stylesheet name without .css extension.
     */
    public void setIconFontName(String iconFontName) {
        this.attributes.put(ICONFONT_NAME, iconFontName);
    }

    /**
     * Sets data-uri attribute.
     * 
     * @param dataUri
     *            true if images should be embedded, false otherwise.
     */
    public void setDataUri(boolean dataUri) {
        this.attributes.put(DATA_URI, toAsciidoctorFlag(dataUri));
    }

    /**
     * Sets icons directory.
     * 
     * @param iconsDir
     */
    public void setIconsDir(String iconsDir) {
        this.attributes.put(ICONS_DIR, iconsDir);
    }

    /**
     * auto-number section titles in the HTML backend
     * 
     * @param sectionNumbers
     */
    public void setSectionNumbers(boolean sectionNumbers) {
        this.attributes.put(SECTION_NUMBERS, toAsciidoctorFlag(sectionNumbers));
    }

    /**
     * Sets linkattrs attribute.
     * 
     * @param linkAttrs
     *            true if Asciidoctor should parse link macro attributes, false otherwise.
     */
    public void setLinkAttrs(boolean linkAttrs) {
        this.attributes.put(LINK_ATTRS, toAsciidoctorFlag(linkAttrs));
    }

    /**
     * Sets experimental attribute.
     * 
     * @param experimental
     *            true if experimental features should be enabled, false otherwise.
     */
    public void setExperimental(boolean experimental) {
        this.attributes.put(EXPERIMENTAL, experimental);
    }

    /**
     * Sets nofooter attribute.
     * 
     * @param noFooter
     *            true if the footer block should not be shown, false otherwise.
     */
    public void setNoFooter(boolean noFooter) {
        this.attributes.put(NO_FOOTER, toAsciidoctorFlag(noFooter));
    }

    /**
     * Sets compat-mode attribute.
     * 
     * @param compatMode
     *            value.
     */
    public void setCompatMode(CompatMode compatMode) {
        this.attributes.put(COMPAT_MODE, compatMode.getMode());
    }

    public void setAttribute(String attributeName, Object attributeValue) {
        this.attributes.put(attributeName, attributeValue);
    }

    /**
     * Sets attributes in string form. An example of a valid string would be:
     * 
     * 'toc numbered source-highlighter=coderay'
     * 
     * where you are adding three attributes: toc, numbered and source-highlighter with value coderay.
     * 
     * @param attributes
     *            in string format.
     */
    public void setAttributes(String attributes) {

        String[] allAttributes = attributes.split(" ");
        addAttributes(allAttributes);
    }

    /**
     * Sets attributes in array form. An example of a valid array would be:
     * 
     * '['toc', 'numbered']'
     * 
     * where you are adding three attributes: toc and numbered.
     * 
     * @param attributes
     *            in array format.
     */
    public void setAttributes(String... attributes) {
        addAttributes(attributes);
    }

    private void addAttributes(String[] allAttributes) {
        for (String attribute : allAttributes) {
            int equalsIndex = attribute.indexOf(ATTRIBUTE_SEPARATOR);
            if (equalsIndex > -1) {
                extractAttributeNameAndValue(attribute, equalsIndex);
            } else {
                this.attributes.put(attribute, "");
            }
        }
    }

    private void extractAttributeNameAndValue(String attribute, int equalsIndex) {
        String attributeName = attribute.substring(0, equalsIndex);
        String attributeValue = attribute.substring(equalsIndex + 1, attribute.length());

        this.attributes.put(attributeName, attributeValue);
    }

    /**
     * Adds all attributes.
     * 
     * @param attributes
     *            to add.
     */
    public void setAttributes(Map<String, Object> attributes) {
        this.attributes.putAll(attributes);
    }

    public static String toAsciidoctorFlag(boolean flag) {
        return flag ? "" : null;
    }

    private static String toDate(Date date) {
        return DATE_FORMAT.format(date);
    }

    private static String toTime(Date time) {
        return TIME_FORMAT.format(time);
    }

    /**
     * @deprecated For internal use only.
     */
    @Deprecated
    public Map<String, Object> map() {
        return this.attributes;
    }
} public class Options {

    public static final String IN_PLACE = "in_place";
    public static final String ATTRIBUTES = "attributes";
    public static final String HEADER_FOOTER = "header_footer";
    public static final String TEMPLATE_DIRS = "template_dirs";
    public static final String TEMPLATE_ENGINE = "template_engine";
    public static final String TO_FILE = "to_file";
    public static final String TO_DIR = "to_dir";
    public static final String MKDIRS = "mkdirs";
    public static final String SAFE = "safe";
    public static final String SOURCEMAP = "sourcemap";
    public static final String STANDALONE = "standalone";
    public static final String ERUBY = "eruby";
    public static final String CATALOG_ASSETS = "catalog_assets";
    public static final String COMPACT = "compact";
    public static final String DESTINATION_DIR = "destination_dir";
    public static final String SOURCE_DIR = "source_dir";
    public static final String BACKEND = "backend";
    public static final String DOCTYPE = "doctype";
    public static final String BASEDIR = "base_dir";
    public static final String TEMPLATE_CACHE = "template_cache";
    public static final String SOURCE = "source";
    public static final String PARSE = "parse";
    public static final String PARSE_HEADER_ONLY = "parse_header_only";

    private Map<String, Object> options = new HashMap<>();

    /**
     * @deprecated Use {@link Options#builder()} instead.
     */
    @Deprecated
    public Options() {
        super();
    }

    /**
     * @deprecated Use {@link Options#builder()} instead.
     */
    @Deprecated
    public Options(Map<String, Object> options) {
        this.options = options;
    }

    /**
     * @return Empty OptionsBuilder instance.
     */
    public static OptionsBuilder builder() {
        return new OptionsBuilder();
    }
    
    public void setInPlace(boolean inPlace) {
        this.options.put(IN_PLACE, inPlace);
    }

    public void setAttributes(Attributes attributes) {
        this.options.put(ATTRIBUTES, attributes.map());
    }

    public void setAttributes(Map<String, Object> attributes) {
        this.options.put(ATTRIBUTES, attributes);
    }

    /**
     * Toggle including header and footer into the output.
     *
     * @param headerFooter If <code>true</true>, include header and footer into the output,
     *                     otherwise exclude them. This overrides any output-specific defaults.
     *
     */
    public void setHeaderFooter(boolean headerFooter) {
        this.options.put(HEADER_FOOTER, headerFooter);
    }

    public void setTemplateDirs(String... templateDirs) {

        if (!this.options.containsKey(TEMPLATE_DIRS)) {
            this.options.put(TEMPLATE_DIRS, new ArrayList<Object>());
        }

        List<Object> allTemplateDirs = (List<Object>) this.options.get(TEMPLATE_DIRS);
        allTemplateDirs.addAll(Arrays.asList(templateDirs));
    }

    public void setTemplateEngine(String templateEngine) {
        this.options.put(TEMPLATE_ENGINE, templateEngine);
    }

    /**
     * Enable writing output to a file. The file includes header and footer by default.
     *
     * @param toFile The path to the output file. If the path is not absolute, it is
     *               interpreted relative to what was set via {@link #setToDir(String)}}
     *               or {@link #setBaseDir(String)}}, in that order.
     *
     */
    public void setToFile(String toFile) {
        this.options.put(TO_FILE, toFile);
    }

    public void setToStream(OutputStream toStream) {
        this.options.put(TO_FILE, toStream);
    }

    /**
     * Toogle writing output to a file.
     *
     * @param toFile If <code>true</true>, write output to a file in the same directory
     *               as the input file, including header and footer into the output. If
     *               <code>false</code>, return output as a string without any header or
     *               footer. The default header and footer visibility can be overridden
     *               using {@link #setHeaderFooter(boolean)}.
     *
     */
    public void setToFile(boolean toFile) {
        this.options.put(TO_FILE, toFile);
    }

    public void setToDir(String toDir) {
        this.options.put(TO_DIR, toDir);
    }

    public void setMkDirs(boolean mkDirs) {
        this.options.put(MKDIRS, mkDirs);
    }

    /**
     * Safe method calls safeMode.getLevel() to put the required level.
     * 
     * @param safeMode
     *            enum.
     */
    public void setSafe(SafeMode safeMode) {
        this.options.put(SAFE, safeMode.getLevel());
    }

    /**
     * Keeps track of the file and line number for each parsed block. (Useful for tooling applications where the association between the converted output and the source file is important).
     * 
     * @param sourcemap
     *            value.
     */
    public void setSourcemap(boolean sourcemap) {
        this.options.put(SOURCEMAP, sourcemap);
    }

    public void setEruby(String eruby) {
        this.options.put(ERUBY, eruby);
    }

    /**
     * If true, tells the parser to capture images and links in the reference table. (Normally only IDs, footnotes and indexterms are included). The reference table is available via the references property on the document AST object. (Experimental).
     * 
     * @param catalogAssets
     *            value.
     */
    public void setCatalogAssets(boolean catalogAssets) {
        this.options.put(CATALOG_ASSETS, catalogAssets);
    }

    public void setCompact(boolean compact) {
        this.options.put(COMPACT, compact);
    }

    public void setDestinationDir(String destinationDir) {
        this.options.put(DESTINATION_DIR, destinationDir);
    }

    public void setSourceDir(String srcDir) {
        this.options.put(SOURCE_DIR, srcDir);
    }

    public void setBackend(String backend) {
        this.options.put(BACKEND, backend);
    }

    public void setDocType(String docType) {
        this.options.put(DOCTYPE, docType);
    }

    public void setBaseDir(String baseDir) {
        this.options.put(BASEDIR, baseDir);
    }

    public void setTemplateCache(boolean templateCache) {
        this.options.put(TEMPLATE_CACHE, templateCache);
    }

    /**
     * If true, the source is parsed eagerly (i.e., as soon as the source is passed to the load or load_file API). If false, parsing is deferred until the parse method is explicitly invoked.
     * 
     * @param parse
     *            value.
     */
    public void setParse(boolean parse) {
        this.options.put(PARSE, parse);
    }

    public void setParseHeaderOnly(boolean parseHeaderOnly) {
        this.options.put(PARSE_HEADER_ONLY, parseHeaderOnly);
    }

    public void setOption(String optionName, Object optionValue) {
        this.options.put(optionName, optionValue);
    }

    /**
     * @deprecated For internal use only.
     */
    @Deprecated
    public Map<String, Object> map() {
        return this.options;
    }

} public interface Document extends StructuralNode {

    /**
     * @return The Title structure for this document.
     * @see Title
     */
    Title getStructuredDoctitle();

    /**
     * @return The title as a String.
     * @see Title
     */
    String getDoctitle();

    /**
     * @deprecated Please use {@link #getDoctitle()}
     * @return The title as a String.
     * @see Title
     */
    @Deprecated
    String doctitle();

    /**
     * Gets the author(s) information as defined in the author line
     * in the document header, or in author & email attributes.
     *
     * @return authors information
     */
    List<Author> getAuthors();

    /**
     * @return basebackend attribute value
     */
    boolean isBasebackend(String backend);

    /**
     * @deprecated Please use {@link #isBasebackend(String)}
     * @return basebackend attribute value
     */
    @Deprecated
    boolean basebackend(String backend);

    /**
     *
     * @return options defined in document.
     */
    Map<Object, Object> getOptions();

    /**
     * Gets the current counter with the given name and increases its value.
     * At the first invocation the counter will return 1.
     * After the call the value of the counter is set to the returned value plus 1.
     * @param name
     * @return
     */
    int getAndIncrementCounter(String name);

    /**
     * Gets the current counter with the given name and increases its value.
     * At the first invocation the counter will return the given initial value.
     * After the call the value of the counter is set to the returned value plus 1.
     * @param name
     * @param initialValue
     * @return
     */
    int getAndIncrementCounter(String name, int initialValue);

    /**
     * @return Whether the sourcemap is enabled.
     */
    boolean isSourcemap();

    /**
     * Toggles the sourcemap option.
     *
     * This method must be called before the document is parsed, such as
     * from a Preprocessor extension. Otherwise, it has no effect.
     *
     * @param state The state in which to put the sourcemap (true = on, false = off).
     */
    void setSourcemap(boolean state);


    /**
     * The catalog contains data collected by asciidoctor that is useful to a converter.
     *
     * Note that the catalog is not part of the asciidoctor public API and is subject to change.
     *
     * @return catalog
     */
    Catalog getCatalog();


    /**
     * The revision information with: date, number and remark.
     *
     * @return revisionInfo
     */
    RevisionInfo getRevisionInfo();
} @Deprecated
public interface DocumentHeader {

    /**
     * @deprecated Use {@link Document#getAuthors()} instead.
     */
    @Deprecated
    List<Author> getAuthors();

    /**
     * @deprecated Use {@link Document#getStructuredDoctitle()} instead.
     */
    @Deprecated
    Title getDocumentTitle();

    /**
     * @deprecated Use {@link Document#getDoctitle()} instead.
     */
    @Deprecated
    String getPageTitle();

    /**
     * @deprecated Use {@link Document#getAuthors()} instead.
     */
    @Deprecated
    Author getAuthor();

    /**
     * @deprecated Use {@link Document#getRevisionInfo()} instead.
     */
    @Deprecated
    RevisionInfo getRevisionInfo();

    /**
     * @deprecated Use {@link Document#getAttributes()} instead.
     */
    @Deprecated
    Map<String, Object> getAttributes();
} public interface AsciidoctorJRuby extends Asciidoctor {


  /**
   * Factory for creating a new instance of Asciidoctor interface.
   *
   * @author lordofthejars
   */
  public static final class Factory {

    private Factory() {
    }

    /**
     * Creates a new instance of Asciidoctor.
     *
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     * Ruby calls.
     */
    public static AsciidoctorJRuby create() {
      return JRubyAsciidoctor.create();
    }

    /**
     * Creates a new instance of Asciidoctor and sets GEM_PATH environment
     * variable to provided gemPath. This method is mostly used in OSGi
     * environments.
     *
     * @param gemPath where gems are located.
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     * Ruby calls.
     */
    public static AsciidoctorJRuby create(String gemPath) {
      return JRubyAsciidoctor.create(gemPath);
    }

    /**
     * Creates a new instance of Asciidoctor and sets loadPath to provided paths. This method is mostly used in OSGi
     * environments.
     *
     * @param loadPaths where Ruby libraries are located.
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     * Ruby calls.
     */
    public static AsciidoctorJRuby create(List<String> loadPaths) {
      return JRubyAsciidoctor.create(loadPaths);
    }

    /**
     * Creates a new instance of Asciidoctor and sets a specific classloader for the  JRuby runtime to use.
     * This method is for use in environments like OSGi.
     * To initialize Asciidoctor in OSGi create the Asciidoctor instance like this:
     *
     * <pre>
     * org.jruby.javasupport.JavaEmbedUtils.initialize(Arrays.asList("uri:classloader:/gems/asciidoctor-1.5.8/lib"));
     * Asciidoctor asciidoctor = Asciidoctor.Factory.create(this.getClass().getClassLoader());
     * </pre>
     *
     * @param classloader
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     *         Ruby calls.
     */
    public static AsciidoctorJRuby create(ClassLoader classloader) {
      return JRubyAsciidoctor.create(classloader);
    }

    /**
     * Creates a new instance of Asciidoctor and sets a specific classloader and gempath for the JRuby runtime to use.
     * This method is for use in environments like OSGi.
     * To initialize Asciidoctor in OSGi create the Asciidoctor instance like this:
     *
     * <pre>
     * org.jruby.javasupport.JavaEmbedUtils.initialize(Arrays.asList("uri:classloader:/gems/asciidoctor-1.5.8/lib"));
     * Asciidoctor asciidoctor = Asciidoctor.Factory.create(this.getClass().getClassLoader());
     * </pre>
     *
     * @param classloader
     * @param gemPath
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     */
    public static AsciidoctorJRuby create(ClassLoader classloader, String gemPath) {
      return JRubyAsciidoctor.create(classloader, gemPath);
    }

    /**
     * Creates a new instance of Asciidoctor and sets loadPath to provided paths.
     * The gem path of the Ruby instance is set to the gemPath parameter.
     *
     * @param loadPaths where Ruby libraries are located.
     * @param gemPath   where gems are located.
     * @return Asciidoctor instance which uses JRuby to wraps Asciidoctor
     * Ruby calls.
     */
    public static AsciidoctorJRuby create(List<String> loadPaths, String gemPath) {
      return JRubyAsciidoctor.create(loadPaths, gemPath);
    }

  }


} public class AuthorImpl implements Author {

    private static final String AUTHOR_KEY_NAME = "name";
    private static final String LASTNAME_KEY_NAME = "lastname";
    private static final String FIRST_NAME_KEY_NAME = "firstname";
    private static final String EMAIL_KEY_NAME = "email";
    private static final String INITIALS_KEY_NAME = "initials";
    private static final String MIDDLE_NAME_KEY_NAME = "middlename";

    private String fullName;
    private String lastName;
    private String firstName;
    private String middleName;
    private String email;
    private String initials;

    public static Author getInstance(RubyStruct authorStruct) {
        final AuthorImpl author = new AuthorImpl();
        author.setFullName(aref(authorStruct, AUTHOR_KEY_NAME));
        author.setFirstName(aref(authorStruct, FIRST_NAME_KEY_NAME));
        author.setMiddleName(aref(authorStruct, MIDDLE_NAME_KEY_NAME));
        author.setLastName(aref(authorStruct, LASTNAME_KEY_NAME));
        author.setInitials(aref(authorStruct, INITIALS_KEY_NAME));
        author.setEmail(aref(authorStruct, EMAIL_KEY_NAME));
        return author;
    }

    private static String aref(RubyStruct s, String key) {
        return (String) JavaEmbedUtils.rubyToJava(s.aref(s.getRuntime().newString(key)));
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getMiddleName() {
        return middleName;
    }

    public void setMiddleName(String middleName) {
        this.middleName = middleName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getInitials() {
        return initials;
    }

    public void setInitials(String initials) {
        this.initials = initials;
    }

    @Override
    public String toString() {

        StringBuilder authorStringRepresentation = new StringBuilder();

        if (this.getFullName() != null) {
            authorStringRepresentation.append(this.getFullName());
        }

        if (this.getEmail() != null) {
            authorStringRepresentation.append(" <").append(this.getEmail()).append(">");
        }

        return authorStringRepresentation.toString();
    }

} public class DocumentHeaderImpl implements DocumentHeader {

    private Title documentTitle;
    private String pageTitle;
    private List<Author> authors;
    private RevisionInfo revisionInfo;

    private Map<String, Object> attributes;

    private DocumentHeaderImpl() {
        super();
    }

    public List<Author> getAuthors() {
        return this.authors;
    }

    public Title getDocumentTitle() {
        return documentTitle;
    }

    public String getPageTitle() {
        return pageTitle;
    }

    public Author getAuthor() {
        return authors == null || authors.size() == 0 ? null : authors.get(0);
    }

    public RevisionInfo getRevisionInfo() {
        return revisionInfo;
    }

    public Map<String, Object> getAttributes() {
        return attributes;
    }

    public static DocumentHeaderImpl createDocumentHeader(Title documentTitle, String pageTitle,
                                                          List<Author> authors, Map<String, Object> attributes) {

        DocumentHeaderImpl documentHeader = new DocumentHeaderImpl();

        documentHeader.documentTitle = documentTitle;
        documentHeader.pageTitle = pageTitle;
        documentHeader.attributes = new CaseInsensitiveMap<>(attributes);
        documentHeader.authors = authors;
        documentHeader.revisionInfo = getRevisionInfo(attributes);

        return documentHeader;
    }

    private static RevisionInfo getRevisionInfo(Map<String, Object> attributes) {
        return RevisionInfoImpl.getInstance(attributes);
    }

} public class ExtensionRegistryExecutor {

    private AsciidoctorJRuby asciidoctor;

    public ExtensionRegistryExecutor(AsciidoctorJRuby asciidoctor) {
        this.asciidoctor = asciidoctor;
    }

    public void registerAllExtensions() {
        ServiceLoader<ExtensionRegistry> extensionRegistryServiceLoader = ServiceLoader
                .load(ExtensionRegistry.class);
        
        for (ExtensionRegistry extensionRegistry : extensionRegistryServiceLoader) {
            extensionRegistry.register(asciidoctor);
        }
    }
} public class JavaLogger extends RubyObject {

  private final LogHandler rootLogHandler;

  private static final String LOG_PROPERTY_SOURCE_LOCATION = "source_location";
  private static final String LOG_PROPERTY_TEXT = "text";

  public static void install(final Ruby runtime, final LogHandler logHandler) {

    final RubyModule asciidoctorModule = runtime.getModule("Asciidoctor");
    final RubyModule loggerManager = asciidoctorModule.defineOrGetModuleUnder("LoggerManager");
    final RubyClass loggerBaseClass = asciidoctorModule.getClass("Logger");

    final RubyClass loggerClass = asciidoctorModule
        .defineOrGetModuleUnder("LoggerManager")
        .defineClassUnder("JavaLogger", loggerBaseClass, new ObjectAllocator() {
          @Override
          public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
            return new JavaLogger(runtime, klazz, logHandler);
          }
        });

    loggerClass.defineAnnotatedMethods(JavaLogger.class);

    final IRubyObject logger = loggerClass.allocate();
    logger.callMethod(runtime.getCurrentContext(), "initialize", runtime.getNil());
    loggerManager.callMethod("logger=", logger);

  }

  private JavaLogger(final Ruby runtime, final RubyClass metaClass, final LogHandler rootLogHandler) {
    super(runtime, metaClass);
    this.rootLogHandler = rootLogHandler;
  }

  @JRubyMethod(name = "initialize", required = 1, optional = 2)
  public IRubyObject initialize(final ThreadContext threadContext, final IRubyObject[] args) {
    return Helpers.invokeSuper(
        threadContext,
        this,
        getMetaClass(),
        "initialize",
        args,
        Block.NULL_BLOCK);
  }

  /**
   * @param threadContext
   * @param args
   */
  @JRubyMethod(name = "add", required = 1, optional = 2)
  public IRubyObject add(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    final IRubyObject rubyMessage;
    if (args.length >= 2 && !args[1].isNil()) {
      rubyMessage = args[1];
    } else if (block.isGiven()) {
      rubyMessage = block.yield(threadContext, getRuntime().getNil());
    } else {
      rubyMessage = args[2];
    }
    final Cursor cursor = getSourceLocation(rubyMessage);
    final String message = formatMessage(rubyMessage);
    final Severity severity = mapRubyLogLevel(args[0]);

    final LogRecord record = createLogRecord(threadContext, severity, cursor, message);

    rootLogHandler.log(record);
    return getRuntime().getNil();
  }

  @JRubyMethod(name = "fatal", required = 0, optional = 2)
  public IRubyObject fatal(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    return log(threadContext, args, block, Severity.FATAL);
  }

  @JRubyMethod(name = "fatal?")
  public IRubyObject fatal(final ThreadContext threadContext) {
    return getRuntime().getTrue();
  }

  @JRubyMethod(name = "error", required = 0, optional = 2)
  public IRubyObject error(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    return log(threadContext, args, block, Severity.ERROR);
  }

  @JRubyMethod(name = "error?")
  public IRubyObject error(final ThreadContext threadContext) {
    return getRuntime().getTrue();
  }

  @JRubyMethod(name = "warn", required = 0, optional = 2)
  public IRubyObject warn(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    return log(threadContext, args, block, Severity.WARN);
  }

  @JRubyMethod(name = "warn?")
  public IRubyObject warn(final ThreadContext threadContext) {
    return getRuntime().getTrue();
  }

  @JRubyMethod(name = "info", required = 0, optional = 2)
  public IRubyObject info(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    return log(threadContext, args, block, Severity.INFO);
  }

  @JRubyMethod(name = "info?")
  public IRubyObject info(final ThreadContext threadContext) {
    return getRuntime().getTrue();
  }

  @JRubyMethod(name = "debug", required = 0, optional = 2)
  public IRubyObject debug(final ThreadContext threadContext, final IRubyObject[] args, Block block) {
    return log(threadContext, args, block, Severity.DEBUG);
  }

  @JRubyMethod(name = "debug?")
  public IRubyObject debug(final ThreadContext threadContext) {
    return getRuntime().getTrue();
  }


  private IRubyObject log(ThreadContext threadContext, IRubyObject[] args, Block block, Severity severity) {
    final IRubyObject rubyMessage;
    if (block.isGiven()) {
      rubyMessage = block.yield(threadContext, getRuntime().getNil());
    } else {
      rubyMessage = args[0];
    }
    final Cursor cursor = getSourceLocation(rubyMessage);
    final String message = formatMessage(rubyMessage);

    final LogRecord record = createLogRecord(threadContext, severity, cursor, message);

    rootLogHandler.log(record);
    return getRuntime().getNil();
  }


  private LogRecord createLogRecord(final ThreadContext threadContext,
                                    final Severity severity,
                                    final Cursor cursor,
                                    final String message) {
    final Optional<BacktraceElement> elem = threadContext.getBacktrace(0)
            .skip(1)
            .findFirst();

    final String sourceFileName = elem.map(BacktraceElement::getFilename).orElse(null);
    final String sourceMethodName = elem.map(BacktraceElement::getMethod).orElse(null);
    final LogRecord record = new LogRecord(severity, cursor, message, sourceFileName, sourceMethodName);
    return record;
  }

  private Severity mapRubyLogLevel(IRubyObject arg) {
    final int rubyId = arg.convertToInteger().getIntValue();
    switch (rubyId) {
      case 0:
        return Severity.DEBUG;
      case 1:
        return Severity.INFO;
      case 2:
        return Severity.WARN;
      case 3:
        return Severity.ERROR;
      case 4:
        return Severity.FATAL;
      default:
        return Severity.UNKNOWN;
    }
  }

  private String formatMessage(final IRubyObject msg) {
    if (getRuntime().getString().equals(msg.getType())) {
      return msg.asJavaString();
    } else if (getRuntime().getHash().equals(msg.getType())) {
      final RubyHash hash = (RubyHash) msg;
      return Objects.toString(hash.get(getRuntime().newSymbol(LOG_PROPERTY_TEXT)));
    }
    throw new IllegalArgumentException(Objects.toString(msg));
  }

  private Cursor getSourceLocation(IRubyObject msg) {
    if (getRuntime().getHash().equals(msg.getType())) {
      final RubyHash hash = (RubyHash) msg;
      final Object sourceLocation = hash.get(getRuntime().newSymbol(LOG_PROPERTY_SOURCE_LOCATION));
      if (sourceLocation != null) {
        return new CursorImpl((IRubyObject) sourceLocation);
      }
    }
    return null;
  }
} public class SyntaxHighlighterRegistryExecutor {

    private AsciidoctorJRuby asciidoctor;

    public SyntaxHighlighterRegistryExecutor(AsciidoctorJRuby asciidoctor) {
        this.asciidoctor = asciidoctor;
    }

    public void registerAllSyntaxHighlighter() {
        ServiceLoader<SyntaxHighlighterRegistry> serviceLoader = ServiceLoader
                .load(SyntaxHighlighterRegistry.class);
        
        for (SyntaxHighlighterRegistry extensionRegistry : serviceLoader) {
            extensionRegistry.register(asciidoctor);
        }
    }
} public interface LogHandler {

    /**
     * Process a log record.
     * This method is called during conversion and will abort completely
     * if an exception is thrown from within.
     *
     * @param logRecord
     */
    void log(LogRecord logRecord);

} public class LogRecord {

    private final Severity severity;

    private Cursor cursor;

    private final String message;

    private String sourceFileName;

    private String sourceMethodName;

    public LogRecord(Severity severity, String message) {
        this.severity = severity;
        this.message = message;
    }

    public LogRecord(Severity severity, Cursor cursor, String message) {
        this.severity = severity;
        this.cursor = cursor;
        this.message = message;
    }

    public LogRecord(Severity severity, Cursor cursor, String message, String sourceFileName, String sourceMethodName) {
        this.severity = severity;
        this.cursor = cursor;
        this.message = message;
        this.sourceFileName = sourceFileName;
        this.sourceMethodName = sourceMethodName;
    }

    /**
     * @return Severity level of the current record.
     */
    public Severity getSeverity() {
        return severity;
    }

    /**
     * @return Information about the location of the event.
     */
    public Cursor getCursor() {
        return cursor;
    }

    /**
     * @return Descriptive message about the event.
     */
    public String getMessage() {
        return message;
    }

    /**
     * @return The value <code>&lt;script&gt;</code>. For the source filename use {@link Cursor#getFile()} obtained with the {@link #getCursor()} method.
     */
    public String getSourceFileName() {
        return sourceFileName;
    }

    /**
     * @return The Asciidoctor Ruby engine method used to convert the file; <code>convertFile</code> or <code>convert</code> whether you are converting a File or a String.
     */
    public String getSourceMethodName() {
        return sourceMethodName;
    }
} public interface SyntaxHighlighterRegistry {

    void register(Class<? extends SyntaxHighlighterAdapter> sourceHighlighter , String... names);

}
import org.asciidoctor.Attributes;
import org.asciidoctor.Options;
import org.asciidoctor.OptionsBuilder;
import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.DocumentHeader;
import org.asciidoctor.converter.JavaConverterRegistry;
import org.asciidoctor.extension.ExtensionGroup;
import org.asciidoctor.extension.JavaExtensionRegistry;
import org.asciidoctor.extension.RubyExtensionRegistry;
import org.asciidoctor.jruby.AsciidoctorJRuby;
import org.asciidoctor.jruby.DirectoryWalker;
import org.asciidoctor.jruby.ast.impl.AuthorImpl;
import org.asciidoctor.jruby.ast.impl.DocumentHeaderImpl;
import org.asciidoctor.jruby.ast.impl.NodeConverter;
import org.asciidoctor.jruby.converter.internal.ConverterRegistryExecutor;
import org.asciidoctor.jruby.extension.internal.ExtensionRegistryExecutor;
import org.asciidoctor.jruby.log.internal.JULLogHandler;
import org.asciidoctor.jruby.log.internal.JavaLogger;
import org.asciidoctor.jruby.log.internal.LogHandlerRegistryExecutor;
import org.asciidoctor.jruby.syntaxhighlighter.internal.SyntaxHighlighterRegistryExecutor;
import org.asciidoctor.log.LogHandler;
import org.asciidoctor.log.LogRecord;
import org.asciidoctor.syntaxhighlighter.SyntaxHighlighterRegistry;
import org.jruby.*;
import org.jruby.exceptions.RaiseException;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.runtime.builtin.IRubyObject;
import java.io.*;
import java.util.*;
import java.util.logging.Logger;
package org.asciidoctor.jruby.internal;



public class JRubyAsciidoctor implements AsciidoctorJRuby, LogHandler {

    private static final Logger logger = Logger.getLogger(JRubyAsciidoctor.class.getName());

    private static final String GEM_PATH = "GEM_PATH";

    protected RubyGemsPreloader rubyGemsPreloader;

    protected Ruby rubyRuntime;

    private RubyClass extensionGroupClass;

    private List<LogHandler> logHandlers = new ArrayList<>();

    public JRubyAsciidoctor() {
        this(createRubyRuntime(Collections.singletonMap(GEM_PATH, null), new ArrayList<>(), null));
        processRegistrations(this);
    }

    private JRubyAsciidoctor(final Ruby rubyRuntime) {
        this.rubyRuntime = rubyRuntime;

        InputStream inputStream = getClass().getResourceAsStream("asciidoctorclass.rb");
        final String script = IOUtils.readFull(inputStream);
        this.rubyRuntime.evalScriptlet(script);

        this.rubyGemsPreloader = new RubyGemsPreloader(this.rubyRuntime);
        this.logHandlers.add(new JULLogHandler());
    }

    public static JRubyAsciidoctor create() {
        return create((String) null);
    }

    public static JRubyAsciidoctor create(String gemPath) {
        return processRegistrations(createJRubyAsciidoctorInstance(Collections.singletonMap(GEM_PATH, gemPath), new ArrayList<>(), null));
    }

    public static JRubyAsciidoctor create(List<String> loadPaths) {
        return processRegistrations(createJRubyAsciidoctorInstance(null, loadPaths, null));
    }

    public static JRubyAsciidoctor create(ClassLoader classloader) {
        return processRegistrations(createJRubyAsciidoctorInstance(null, new ArrayList<>(), classloader));
    }

    public static JRubyAsciidoctor create(ClassLoader classloader, String gemPath) {
        return processRegistrations(createJRubyAsciidoctorInstance(Collections.singletonMap(GEM_PATH, gemPath), new ArrayList<>(), classloader));
    }

    public static JRubyAsciidoctor create(List<String> loadPaths, String gemPath) {
        return processRegistrations(createJRubyAsciidoctorInstance(Collections.singletonMap(GEM_PATH, gemPath), loadPaths, null));
    }

    private static JRubyAsciidoctor processRegistrations(JRubyAsciidoctor asciidoctor) {
        registerExtensions(asciidoctor);
        registerConverters(asciidoctor);
        registerSyntaxHighlighters(asciidoctor);
        registerLogHandlers(asciidoctor);

        JavaLogger.install(asciidoctor.getRubyRuntime(), asciidoctor);

        return asciidoctor;
    }

    private static void registerConverters(AsciidoctorJRuby asciidoctor) {
        new ConverterRegistryExecutor(asciidoctor).registerAllConverters();
    }

    private static void registerExtensions(AsciidoctorJRuby asciidoctor) {
        new ExtensionRegistryExecutor(asciidoctor).registerAllExtensions();
    }

    private static void registerSyntaxHighlighters(AsciidoctorJRuby asciidoctor) {
        new SyntaxHighlighterRegistryExecutor(asciidoctor).registerAllSyntaxHighlighter();
    }

    private static void registerLogHandlers(AsciidoctorJRuby asciidoctor) {
        new LogHandlerRegistryExecutor(asciidoctor).registerAllLogHandlers();
    }

    private static JRubyAsciidoctor createJRubyAsciidoctorInstance(Map<String, String> environmentVars, List<String> loadPaths, ClassLoader classloader) {
        Ruby rubyRuntime = createRubyRuntime(environmentVars, loadPaths, classloader);
        return new JRubyAsciidoctor(rubyRuntime);
    }

    private static Ruby createRubyRuntime(Map<String, String> environmentVars, List<String> loadPaths, ClassLoader classloader) {
        Map<String, String> env = environmentVars != null ? new HashMap<>(environmentVars) : new HashMap<>();

        RubyInstanceConfig config = createOptimizedConfiguration();
        if (classloader != null) {
            config.setLoader(classloader);
        }
        injectEnvironmentVariables(config, env);

        return JavaEmbedUtils.initialize(loadPaths, config);
    }

    private static void injectEnvironmentVariables(RubyInstanceConfig config, Map<String, String> environmentVars) {
        EnvironmentInjector environmentInjector = new EnvironmentInjector(config);
        environmentInjector.inject(environmentVars);
    }

    private static RubyInstanceConfig createOptimizedConfiguration() {
        return new RubyInstanceConfig();
    }

    @Override
    public void close() {
        if (rubyRuntime != null) {
            rubyRuntime.tearDown();
        }
    }

    @Override
    public void registerLogHandler(final LogHandler logHandler) {
        if (!this.logHandlers.contains(logHandler)) {
            this.logHandlers.add(logHandler);
        }
    }

    @Override
    public void unregisterLogHandler(final LogHandler logHandler) {
        this.logHandlers.remove(logHandler);
    }

    public Ruby getRubyRuntime() {
        return rubyRuntime;
    }

    private DocumentHeader toDocumentHeader(Document document) {