pax_global_header00006660000000000000000000000064122122051770014511gustar00rootroot0000000000000052 comment=b6ce9e62596371bc8854a1e45d6f0d23492a1cd0 asciidoctor-0.1.4/000077500000000000000000000000001221220517700140165ustar00rootroot00000000000000asciidoctor-0.1.4/.gitignore000066400000000000000000000000431221220517700160030ustar00rootroot00000000000000pkg/ /Gemfile.lock /.bundle /rdoc/ asciidoctor-0.1.4/.travis.yml000066400000000000000000000004241221220517700161270ustar00rootroot00000000000000language: ruby rvm: - 2.0.0 - 1.9.3 - 1.8.7 - jruby-18mode - jruby-19mode # rbx on Travis cannot find ERB, disable until fixed again # - rbx-18mode # - rbx-19mode #script: rake test test_us_ascii notifications: email: false irc: "irc.freenode.org#asciidoctor" asciidoctor-0.1.4/CHANGELOG.adoc000066400000000000000000000426761221220517700161540ustar00rootroot00000000000000= Asciidoctor Changelog http://asciidoctor.org[Asciidoctor] is an open source text processor and publishing toolchain for converting http://asciidoctor.org[AsciiDoc] markup into HTML, DocBook and custom formats. This document provides a high-level view of the changes introduced in Asciidoctor by release. For a detailed view of what has changed, refer to the https://github.com/asciidoctor/asciidoctor/commits/master[commit history] on GitHub. == 0.1.4 (2013-09-05) - @mojavelinux Performance:: * 15% increase in speed compared to 0.1.3 Enhancements:: * updated xref inline macro to support inter-document references (#417) * added extension API for document processing (#79) * added include directive processor extension (#100) * added id and role shorthand for formatted (quoted) text (#517) * added shorthand syntax for specifying block options (#481) * added support for checklists in unordered list (#200) * added support for inline style for unordered lists (#620) * added DocBook 5 backend (#411) * added docinfo option for footer (#486) * added Pygments as source highlighter option (pygments) (#538) * added icon inline macro (#529) * recognize implicit table header row (#387) * uri can be used in inline image (#470) * add float attribute to inline image (#616) * allow role to be specified on text enclosed in backticks (#419) * added XML comment-style callouts for use in XML listings (#582) * made callout bullets non-selectable in HTML output (#478) * pre-wrap literal blocks, added nowrap option to listing blocks (#303) * skip (retain) missing attribute references by default (#523) * added attribute-missing attribute to control how a missing attribute is handled (#495) * added attribute-undefined attribute to control how an undefined attribute is handled (#495) * permit !name syntax for undefining attribute (#498) * ignore front matter used by static site generators if skip-front-matter attribute is set (#502) * sanitize contents of HTML title element in html5 backend (#504) * support toc position for toc2 (#467) * cli accepts multiple files as input (@lordofthejars) (#227) * added Markdown-style horizontal rules and pass Markdown tests (#455) * added float clearing classes (.clearfix, .float-group) (#602) * don't disable syntax highlighting when explicit subs is used on listing block * asciidoctor package now available in Debian Sid and Ubuntu Saucy (@avtobiff) (#216) Compliance:: * embed CSS by default, copy stylesheet when linkcss is set unless copycss! is set (#428) * refactor reader to track include stack (#572) * made include directive resolve relative to current file (#572) * track include stack to enforce maximum depth (#581) * fixed greedy comment blocks and paragraphs (#546) * enable toc and numbered by default in DocBook backend (#540) * ignore comment lines when matching labeled list item (#524) * correctly parse footnotes that contain a URL (#506) * parse manpage metadata, output manpage-specific HTML, set docname and outfilesuffix (#488, #489) * recognize preprocessor directives on first line of AsciiDoc table cell (#453) * include directive can retrieve data from uri if allow-uri-read attribute is set (#445) * support escaping attribute list that precedes formatted (quoted) text (#421) * made improvements to list processing (#472, #469, #364) * support percentage for column widths (#465) * substitute attributes in docinfo files (#403) * numbering no longer increments on unnumbered sections (#393) * fixed false detection of list item with hyphen marker * skip include directives when processing comment blocks * added xmlns to root element in docbook45 backend, set noxmlns attribute to disable * added a Compliance module to control compliance-related behavior * added linkattrs feature to AsciiDoc compatibility file (#441) * added level-5 heading to AsciiDoc compatibility file (#388) * added new XML-based callouts to AsciiDoc compatibility file * added absolute and uri image target matching to AsciiDoc compatibility file * added float attribute on inline image macro to AsciiDoc compatibility file * removed linkcss in AsciiDoc compatibility file * fixed fenced code entry in compatibility file Bug Fixes:: * lowercase attribute names passed to API (#508) * numbered can still be toggled even when enabled in API (#393) * allow JRuby Map as attributes (#396) * don't attempt to highlight callouts when using CodeRay and Pygments (#534) * correctly calculate line length in Ruby 1.8 (#167) * write to specified outfile even when input is stdin (#500) * only split quote attribution on first comma in Markdown blockquotes (#389) * don't attempt to print render times when doc is not rendered * don't recognize line with four backticks as a fenced code block (#611) Improvements:: * upgraded Font Awesome to 3.2.1 (#451) * improved the built-in CodeRay theme to match Asciidoctor styles * link to CodeRay stylesheet if linkcss is set (#381) * style the video block (title & margin) (#590) * added Groovy, Clojure, Python and YAML to floating language hint * only process callouts for blocks in which callouts are found * added content_model to AbstractBlock, rename buffer to lines * use Untitled as document title in rendered output if document has no title * rename include-depth attribute to max-include-depth, set 64 as default value (#591) * the tag attribute can be used on the include directive to identify a single tagged region * output multiple authors in HTML backend (#399) * allow multiple template directories to be specified, document in usage and manpage (#437) * added option to cli to specify template engine (#406) * added support for external video hosting services in video block macro (@xcoulon) (#587) * strip leading separator(s) on section id if idprefix is blank (#551) * customized styling of toc placed inside body content (#507) * consolidate toc attribute so toc with or without toc-position can make sidebar toc (#618) * properly style floating images (inline & block) (#460) * add float attribute to inline images (#616) * use ul list for TOC in HTML5 backend (#431) * support multiple terms per labeled list item in model (#532) * added role?, has_role?, option? and roles methods to AbstractNode (#423, 474) * added captioned_title method to AbstractBlock * honor showtitle attribute as alternate to notitle! (#457) * strip leading indent from literal paragraph blocks assigned the style normal * only process lines in AsciiDoc files * emit message that tilt gem is required to use custom backends if missing (#433) * use attributes for version and last updated messages in footer (#596) * added a basic template cache (#438) * include line info in several of the warnings (for lists and tables) * print warning/error messages using warn (#556) * lines are not preprocessed when peeking ahead for section underline * introduced Cursor object to track line info * fixed table valign classes, no underline on image link * removed dependency on pending library, lock Nokogiri version to 1.5.10 * removed require rubygems line in asciidoctor.rb, add to cli if RUBY_VERSION < 1.9 * added tests for custom backends * added test that shorthand doesn't clobber explicit options (#481) * removed unnecessary monospace class from literal and listing blocks Distribution Packages:: * http://rubygems.org/gems/asciidoctor[RubyGem (asciidoctor)] * https://apps.fedoraproject.org/packages/rubygem-asciidoctor[Fedora (rubygem-asciidoctor)] * http://packages.debian.org/sid/asciidoctor[Debian (asciidoctor)] * http://packages.ubuntu.com/saucy/asciidoctor[Ubuntu (asciidoctor)] https://github.com/asciidoctor/asciidoctor/issues?milestone=7&state=closed[issues resolved] :: https://github.com/asciidoctor/asciidoctor/releases/tag/v0.1.4[git tag] :: https://github.com/asciidoctor/asciidoctor/compare/v0.1.3...v0.1.4[full diff] == 0.1.3 (2013-05-30) - @mojavelinux Performance:: * 10% increase in speed compared to 0.1.2 Enhancements:: * added support for inline rendering by setting doctype to inline (#328) * added support for using font-based icons (#115) * honor haml/slim/jade-style shorthand for id and role attributes (#313) * support Markdown-style headings as section titles (#373) * support Markdown-style quote blocks * added section level 5 (maps to h6 element in the html5 backend) (#334) * added btn inline macro (#259) * added menu inline menu to identify a menu selection (@bleathem) (#173) * added kbd inline macro to identify a key or key combination (@bleathem) (#172) * support alternative quote forms (#196) * added indent attribute to verbatim blocks (#365) * added prettify source-highlighter (#202) * link section titles (#122) * introduce shorthand syntax for table format (#350) * parse attributes in link when use-link-attrs attribute is set (#214) * support preamble toc-placement (#295) * exclude attribute div if quote has no attribution (#309) * support attributes passed to API as string or string array (#289) * allow safe mode to be set using string, symbol or int in API (#290) * make level 0 section titles more prominent in TOC (#369) Compliance:: * ~ 99.5% compliance with AsciiDoc * drop line if target of include directive is blank (#376) * resolve attribute references in target of include directive (#367) * added irc scheme to link detection (#314) * toc should honor numbered attribute (#341) * added toc2 layout to default stylesheet (#285) * consecutive terms in labeled list share same entry (#315) * support set:name:value attribute syntax (#228) * block title not allowed above document title (#175) * assign caption even if no title (#321) * horizontal dlist layout in docbook backend (#298) * set doctitle attribute (#337) * allow any backend to be specified in cli (@lightguard) (#320) * support for abstract and partintro (#297) Bug Fixes:: * fixed file path resolution on Windows (#330) * fixed bad variable name that was causing crash, add test for it (#335) * set proper encoding on input data (#308) * don't leak doctitle into nested document (#382) * handle author(s) defined using attributes (#301) Improvements:: * added tests for all special sections (#80) * added test for attributes defined as string or string array (@lightguard) (#291) Distribution Packages:: * http://rubygems.org/gems/asciidoctor[RubyGem (asciidoctor)] * https://apps.fedoraproject.org/packages/rubygem-asciidoctor[Fedora (rubygem-asciidoctor)] http://asciidoctor.org/news/2013/05/31/asciidoctor-0-1-3-released[release notes] :: https://github.com/asciidoctor/asciidoctor/issues?milestone=4&state=closed[issues resolved] :: https://github.com/asciidoctor/asciidoctor/releases/tag/v0.1.3[git tag] :: https://github.com/asciidoctor/asciidoctor/compare/v0.1.2...v0.1.3[full diff] == 0.1.2 (2013-04-25) - @mojavelinux Performance:: * 28% increase in speed compared to 0.1.1, 32% increase compared to 0.1.0 Enhancements:: * new website at http://asciidoctor.org * added a default stylesheet (#76) * added viewport meta tag for mobile browsers (#238) * set attributes based on safe mode (#244) * added admonition name as style class (#265) * removed hardcoded CSS, no one likes hardcoded CSS (#165) * support multiple authors in document header (#223) * include footnotes block in embedded document (#206) * allow comma delimiter in include attribute values (#226) * support including tagged lines (#226) * added line selection to include directive (#226) * Asciidoctor#render APIs return Document when document is written to file Compliance:: * added AsciiDoc compatibility file to make AsciiDoc behave like Asciidoctor (#257) * restore alpha-based xml entities (#211) * implement video and audio block macros (#155) * implement toc block macro (#269) * correctly handle multi-part books (#222) * complete masquerade functionality for blocks & paragraphs (#187) * support explicit subs on blocks (#220) * use code element instead of tt (#260) * honor toc2 attribute (#221) * implement leveloffset feature (#212) * include docinfo files in header when safe mode < SERVER (#116) * support email links and mailto inline macros (#213) * question must be wrapped in simpara (#231) * allow round bracket in link (#218) Bug Fixes:: * trailing comma shouldn't be included in link (#280) * warn if file in include directive doesn't exist (#262) * negative case for inline ifndef should only affect current line (#241) * don't compact nested documents (#217) * nest revision info inside revision element (#236) Distribution Packages:: * http://rubygems.org/gems/asciidoctor[RubyGem (asciidoctor)] http://asciidoctor.org/news/2013/04/25/asciidoctor-0-1-2-released[release notes] :: https://github.com/asciidoctor/asciidoctor/issues?milestone=3&state=closed[issues resolved] :: https://github.com/asciidoctor/asciidoctor/releases/tag/v0.1.2[git tag] :: https://github.com/asciidoctor/asciidoctor/compare/v0.1.1...v0.1.2[full diff] == 0.1.1 (2013-02-26) - @erebor Performance:: * 15% increase in speed compared to 0.1.0 Enhancements:: * migrated repository to asciidoctor organization on GitHub (#77) * include document title when header/footer disabled and notitle attribute is unset (#103) * honor GitHub-flavored Markdown fenced code blocks (#118) * added :doctype and :backend keys to options hash in API (#163) * added :to_dir option to the Asciidoctor#render API * added option :header_only to stop parsing after reading the header * preliminary line number tracking * auto-select backend sub-folder containing custom templates * rubygem-asciidoctor package now available in Fedora (#92) Compliance:: * refactor reader, process attribute entries and conditional blocks while parsing (#143) * support limited value comparison functionality of ifeval (#83) * added support for multiple attributes in ifdef and ifndef directives * don't attempt to embed image with uri reference when data-uri is set (#157) * accomodate trailing dot in author name (#156) * don't hardcode language attribute in html backend (#185) * removed language from DocBook root node (#188) * fixed revinfo line swallowing attribute entry * auto-generate caption for listing blocks if listing-caption attribute is set * support nested includes * support literal and listing paragraphs * support em dash shorthand at the end of a line * added ftp support to link inline macro * added support for the page break block macro Bug Fixes:: * pass through image with uri reference when data-uri is set (#157) * print message for failed arg (#152) * normalize whitespace at the end of lines (improved) * properly load custom templates and required libraries Improvements:: * parse document header in distinct parsing step * moved hardcoded english captions to attributes Distribution Packages:: * http://rubygems.org/gems/asciidoctor[RubyGem (asciidoctor)] https://github.com/asciidoctor/asciidoctor/issues?milestone=1&state=closed[issues resolved] :: https://github.com/asciidoctor/asciidoctor/releases/tag/v0.1.1[git tag] :: https://github.com/asciidoctor/asciidoctor/compare/v0.1.0...v0.1.1[full diff] == 0.1.0 (2013-02-04) - @erebor Enhancements:: * introduced Asciidoctor API (Asciidoctor#load and Asciidoctor#render methods) (#34) * added SERVER safe mode level (minimum recommended security for serverside usage) (#93) * added the asciidoctor commandline interface (cli) * added asciidoctor-safe command, enables safe mode by default * added man page for the asciidoctor command * use blockquote tag for quote block content (#124) * added hardbreaks option to preserve line breaks in paragraph text (#119) * :header_footer option defaults to false when using the API, unless rendering to file * added idseparator attribute to customized separator used in generated section ids * do not number special sections (differs from AsciiDoc) Compliance:: * use callout icons if icons are enabled, unless safe mode is SECURE * added support for name=value@ attribute syntax passed via cli (#97) * attr refs no longer case sensitive (#109) * fixed several cases of incorrect list handling * don't allow links to consume endlines or surrounding angled brackets * recognize single quote in author name * support horizontal labeled list style * added support for the d cell style * added support for bibliography anchors * added support for special sections (e.g., appendix) * added support for index term inline macros * added support for footnote and footnoteref inline macros * added auto-generated numbered captions for figures, tables and examples * added counter inline macros * added support for floating (discrete) section titles Bug Fixes:: * fixed UTF-8 encoding issue by adding magic encoding line to ERB templates (#144) * resolved Windows compatibility issues * clean CRLF from end of lines (#125) * enabled warnings when running tests, fixed warnings (#69) Improvements:: * renamed iconstype attribute to icontype Distribution Packages:: * http://rubygems.org/gems/asciidoctor[RubyGem (asciidoctor)] https://github.com/asciidoctor/asciidoctor/issues?milestone=12&state=closed[issues resolved] :: https://github.com/asciidoctor/asciidoctor/releases/tag/v0.1.0[git tag] :: https://github.com/asciidoctor/asciidoctor/compare/v0.0.9...v0.1.0[full diff] == Older releases (pre-0.0.1) For information about older releases, refer to the https://github.com/asciidoctor/asciidoctor/tags[commit history] on GitHub. asciidoctor-0.1.4/Gemfile000066400000000000000000000004311221220517700153070ustar00rootroot00000000000000source 'https://rubygems.org' gemspec # enable this group to use Guard for continuous testing # after removing comments, run `bundle install` then `guard` #group :guardtest do # gem 'guard' # gem 'guard-test' # gem 'libnotify' # gem 'listen', :github => 'guard/listen' #end asciidoctor-0.1.4/Guardfile000066400000000000000000000006461221220517700156510ustar00rootroot00000000000000# use `guard start -n f` to disable notifications # or set the environment variable GUARD_NOTIFY=false notification :libnotify, :display_message => true, :timeout => 5, # in seconds :append => false, :transient => true, :urgency => :critical guard :test do watch(%r{^lib/(.+)\.rb$}) do |m| "test/#{m[1]}_test.rb" end watch(%r{^test.+_test\.rb$}) watch('test/test_helper.rb') do "test" end end asciidoctor-0.1.4/LICENSE000066400000000000000000000021041221220517700150200ustar00rootroot00000000000000The MIT License Copyright (C) 2012-2013 Dan Allen and Ryan Waldron Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. asciidoctor-0.1.4/README.adoc000066400000000000000000000542221221220517700156100ustar00rootroot00000000000000= Asciidoctor Dan Allen ; Ryan Waldron ; Sarah White :awestruct-layout: base :homepage: http://asciidoctor.org :asciidoc: http://asciidoc.org :gem: http://rubygems.org/gems/asciidoctor :toolchain: http://asciidoctor.org/docs/install-toolchain :install-mac: http://asciidoctor.org/docs/install-asciidoctor-macosx :render: http://asciidoctor.org/docs/render-documents :factory: http://asciidoctor.org/docs/produce-custom-themes-using-asciidoctor-stylesheet-factory :java: http://asciidoctor.org/docs/install-and-use-asciidoctor-java-integration :man: http://asciidoctor.org/man/asciidoctor :sources: https://github.com/asciidoctor/asciidoctor :tests: https://github.com/asciidoctor/asciidoctor/tree/master/test :issues: https://github.com/asciidoctor/asciidoctor/issues :forum: http://discuss.asciidoctor.org :irc: irc://irc.freenode.org/#asciidoctor :news: http://asciidoctor.org/news :docs: http://asciidoctor.org/docs :org: https://github.com/asciidoctor :contributors: https://github.com/asciidoctor/asciidoctor/graphs/contributors :templates: https://github.com/asciidoctor/asciidoctor/blob/master/lib/asciidoctor/backends :gitscm-next: https://github.com/github/gitscm-next :seed-contribution: https://github.com/github/gitscm-next/commits/master/lib/asciidoc.rb :tilt: https://github.com/rtomayko/tilt :freesoftware: http://www.gnu.org/philosophy/free-sw.html :gist: https://gist.github.com :fork: https://help.github.com/articles/fork-a-repo :branch: http://learn.github.com/p/branching.html :pr: https://help.github.com/articles/using-pull-requests :changelog: https://github.com/asciidoctor/asciidoctor/blob/master/CHANGELOG.adoc :license: https://github.com/asciidoctor/asciidoctor/blob/master/LICENSE :idprefix: :idseparator: - ifndef::safe-mode-name[] [float] = Asciidoctor endif::[] {homepage}[Asciidoctor] is an open source text processor and publishing toolchain for transforming {asciidoc}[AsciiDoc] markup into HTML 5, DocBook 4.5 and 5.0 and other custom formats. Asciidoctor is written entirely in Ruby, packaged as a RubyGem and published to {gem}[RubyGems.org]. There are also Fedora, Debian and Ubuntu packages available for installing Asciidoctor. Asciidoctor is released under the MIT license. ifndef::awestruct[] *Project health:* image:https://travis-ci.org/asciidoctor/asciidoctor.png?branch=master[Build Status, link="https://travis-ci.org/asciidoctor/asciidoctor"] endif::awestruct[] == AsciiDoc Processing Asciidoctor reads and parses AsciiDoc markup (from a file or string) and feeds the parsed result to a set of built-in templates to render the document as HTML 5, DocBook 4.5 or DocBook 5.0. Asciidoctor is a drop-in replacement for the original AsciiDoc processor. We've matched the output to that produced by the AsciiDoc Python processor as faithfully as possible. You can override the built-in templates, or produce a custom format, by pointing the processor at a set of template files written in a language supported by {tilt}[Tilt]. See the <> section for more details. NOTE: With few exceptions, Asciidoctor is compliant with the original AsciiDoc processor. Asciidoctor has well over 1,000 tests to ensure compatibility with the AsciiDoc syntax. We continue to work hard to ensure Asciidoctor continues to serve as a drop-in replacement for AsciiDoc. == Operating Systems Asciidoctor works on Linux, Mac and Windows. == Dependency and Configuration Requirements Asciidoctor requires one of the following implementations of Ruby: * Ruby 1.8.7 * Ruby 1.9.3 * Ruby 2.0.0 * JRuby 1.7.4 * Rubinius 2.0 - _testing suspended until a release is available_ We expect Asciidoctor to work with other versions of Ruby as well. We welcome your help testing those versions if you are interested in seeing them supported. // QUESTION What is considered configuration information? // QUESTION What about other sub-projects of Asciidoctor that require Tilt? // TODO fill in this section and enable //== List of files/directory structure // //Optional depending on the complexity of the project The latest source code is located in the {sources}[Asciidoctor git repository] on GitHub. == Installation Asciidoctor can be installed via the +gem+ command, bundler, or popular Linux package managers. === gem install To install Asciidoctor using the +gem+ command: . Open a terminal . Type the +gem+ command $> gem install asciidoctor === bundle install (Bundler) To install Asciidoctor using bundler: . Open your system Gemfile . Add the +asciidoctor+ gem to your Gemfile using the following text source 'https://rubygems.org' gem 'asciidoctor' . Save the Gemfile . Open a terminal . Install the gem with bundler $> bundle install === yum install (Fedora) To install Asciidoctor on Fedora 17 or greater: . Open a terminal . Type the +yum+ command $> sudo yum install rubygem-asciidoctor The benefit of installing the gem via +yum+ is that the package manager will also install Ruby and RubyGems if not already on your machine. === apt-get install (Debian, Ubuntu) To install Asciidoctor on Debian Sid or Ubuntu Saucy or greater: . Open a terminal . Type the +apt-get+ command $> sudo apt-get install asciidoctor The benefit of installing the gem via +apt-get+ is that the package manager will also install Ruby and RubyGems if not already on your machine. === Other installation options * {toolchain}[Installing the Asciidoctor toolchain] * {install-mac}[Installing Asciidoctor on Mac OS X] == Upgrading If you have an earlier version of Asciidoctor installed, you can update it using the +gem+ command: $> gem update asciidoctor [TIP] ==== If you accidentally use +gem install+ instead of +gem update+ then you will have both versions installed. If you wish to remove the older version use the +gem+ command: $> gem cleanup asciidoctor ==== On Fedora, you can update it using: $> sudo yum update rubygem-asciidoctor TIP: Your Fedora system may be configured to automatically update packages, in which case no further action is required by you. Refer to the http://docs.fedoraproject.org[Fedora docs] if you are unsure. On Debian or Ubuntu, you can update it using: $> sudo apt-get upgrade asciidoctor NOTE: The Fedora, Debian and Ubuntu packages will not be available right away after a release of the RubyGem. It may take several weeks before the packages become available for a new release. If you need the latest version immediately, use the +gem install+ option. == Usage If the Asciidoctor gem installed successfully, the +asciidoctor+ command line interface (CLI) will be available on your PATH. To invoke it, execute: $> asciidoctor --version Asciidoctor 0.1.4 [http://asciidoctor.org] In addition to the CLI, Asciidoctor provides a Ruby API The API is intended for integration with other software projects and is suitable for server-side applications, such as Rails, Sinatra and GitHub. TIP: Asciidoctor also has a Java API that mirrors the Ruby API. The Java API calls through to the Ruby API using an embedded JRuby runtime. See the {java}[Asciidoctor Java integration project] for more information. === Command line interface (CLI) Asciidoctor's CLI is a drop-in replacement for the +asciidoc.py+ command from the Python implementation. To invoke Asciidoctor from the CLI, execute: asciidoctor This will use the built-in defaults for options and create a new file in the same directory as the input file, with the same base name, but with the +.html+ extension. There are many other options available and full help is provided via: asciidoctor --help or in the {man}[man page]. There is also an +asciidoctor-safe+ command, which turns on safe mode by default, preventing access to files outside the parent directory of the source file. This mode is very similar to the safe mode of +asciidoc.py+. Additional documentation: * {render}[How do I render a document?] * {factory}[How do I use the Asciidoctor stylesheet factory to produce custom themes?] === Ruby API To use Asciidoctor in your application, you first need to require the gem: require 'asciidoctor' With that in place, you can start processing AsciiDoc documents. .Loading a document To parse a file into an +Asciidoctor::Document+ object: doc = Asciidoctor.load_file 'sample.adoc' You can get information about the document: puts doc.doctitle puts doc.attributes More than likely, you will want to render the document. .Rendering files -- To render a file containing AsciiDoc markup to HTML 5, use: Asciidoctor.render_file 'sample.adoc', :in_place => true The command will output to the file +sample.html+ in the same directory. You can render the file to DocBook 4.5 by setting the +:backend+ option to +'docbook'+: Asciidoctor.render_file 'sample.adoc', :in_place => true, :backend => 'docbook' The command will output to the file +sample.xml+ in the same directory. (If you're on Linux, you can view the file using yelp). -- .Rendering strings -- To render an AsciiDoc-formatted string: puts Asciidoctor.render '*This* is Asciidoctor.' When rendering a string, the header and footer are excluded by default to make Asciidoctor consistent with other lightweight markup engines like Markdown. If you want the header and footer, just enable it using the +:header_footer+ option: puts Asciidoctor.render '*This* is Asciidoctor.', :header_footer => true Now you'll get a full HTML 5 file. If you only want the inline markup to be processed, set the +:doctype+ option to +'inline'+: puts Asciidoctor.render '*This* is Asciidoctor.', :doctype => 'inline' As before, you can also produce DocBook 4.5: puts Asciidoctor.render '*This* is Asciidoctor.', :header_footer => true, :backend => 'docbook' If you don't like the output you see, you can change it. Any of it! -- .Custom templates -- Asciidoctor allows you to override the {templates}[built-in templates] used to render almost any individual AsciiDoc element. If you provide a directory of {tilt}[Tilt]-compatible templates, named in such a way that Asciidoctor can figure out which template goes with which element, Asciidoctor will use the templates in this directory instead of its built-in templates for any elements for which it finds a matching template. It will fallback to its default templates for everything else. puts Asciidoctor.render '*This* is Asciidoctor.', :header_footer => true, :template_dir => 'templates' The Document and Section templates should begin with +document.+ and +section.+, respectively. The file extension is used by Tilt to determine which view framework it will use to use to render the template. For instance, if you want to write the template in ERB, you'd name these two templates +document.html.erb+ and +section.html.erb+. To use Haml, you'd name them +document.html.haml+ and +section.html.haml+. Templates for block elements, like a Paragraph or Sidebar, would begin with +block_<% end elsif attr? :stylesheet if @safe >= SafeMode::SECURE || (attr? 'linkcss') %> <% else %> <% end end if attr? 'icons', 'font' if !(attr 'iconfont-remote', '').nil? %> <% else %> <% end end case attr 'source-highlighter' when 'coderay' if (attr 'coderay-css', 'class') == 'class' if @safe >= SafeMode::SECURE || (attr? 'linkcss') %> <% else %> <% end end when 'pygments' if (attr 'pygments-css', 'class') == 'class' if @safe >= SafeMode::SECURE || (attr? 'linkcss') %> <% else %> <% end end when 'highlightjs', 'highlight.js' %> <% when 'prettify' %> <% end %><%= (docinfo_content = docinfo).empty? ? nil : %( \#{docinfo_content}) %> class="<%= doctype %><%= (attr? 'toc-class') && (attr? 'toc') && (attr? 'toc-placement', 'auto') ? %( \#{attr 'toc-class'} toc-\#{attr 'toc-position', 'left'}) : nil %>"<%= (attr? 'max-width') ? %( style="max-width: \#{attr 'max-width'};") : nil %>><% unless noheader %> <% end %>
<%= content %>
<% unless !footnotes? || (attr? :nofootnotes) %>

<% footnotes.each do |fn| %>
<%= fn.index %>. <%= fn.text %>
<% end %>
<% end %> EOS end end class EmbeddedTemplate < BaseTemplate def result(node) result_buffer = [] if !node.notitle && node.has_header? id_attr = node.id ? %( id="#{node.id}") : nil result_buffer << %(#{node.header.title}) end result_buffer << node.content if node.footnotes? && !(node.attr? 'nofootnotes') result_buffer << '
' result_buffer << '
' node.footnotes.each do |footnote| result_buffer << %(
#{footnote.index} #{footnote.text}
) end result_buffer << '
' end result_buffer * EOL end def template :invoke_result end end class BlockTocTemplate < BaseTemplate def result(node) doc = node.document return '' unless (doc.attr? 'toc') if node.id id_attr = %( id="#{node.id}") title_id_attr = '' elsif doc.embedded? || !(doc.attr? 'toc-placement') id_attr = ' id="toc"' title_id_attr = ' id="toctitle"' else id_attr = '' title_id_attr = '' end title = node.title? ? node.title : (doc.attr 'toc-title') levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : (doc.attr 'toclevels', 2).to_i role = node.role? ? node.role : (doc.attr 'toc-class', 'toc') %( #{title} #{DocumentTemplate.outline(doc, levels)} \n) end def template :invoke_result end end class BlockPreambleTemplate < BaseTemplate def toc(node) if (node.attr? 'toc') && (node.attr? 'toc-placement', 'preamble') %(\n
#{node.attr 'toc-title'}
#{DocumentTemplate.outline(node.document, (node.attr 'toclevels', 2).to_i)}
) else '' end end def result(node) %(
#{node.content}
#{toc node}
) end def template :invoke_result end end class SectionTemplate < BaseTemplate def result(sec) slevel = sec.level # QUESTION should this check be done in section? if slevel == 0 && sec.special slevel = 1 end htag = "h#{slevel + 1}" id = anchor = link_start = link_end = nil if sec.id id = %( id="#{sec.id}") if sec.document.attr? 'sectanchors' #if sec.document.attr? 'icons', 'font' # anchor = %() #else anchor = %() #end elsif sec.document.attr? 'sectlinks' link_start = %() link_end = '' end end if slevel == 0 %(#{anchor}#{link_start}#{sec.title}#{link_end} #{sec.content}) else role = sec.role? ? " #{sec.role}" : nil if sec.numbered sectnum = "#{sec.sectnum} " else sectnum = nil end if slevel == 1 content = %(
#{sec.content}
) else content = sec.content end %(
<#{htag}#{id}>#{anchor}#{link_start}#{sectnum}#{sec.captioned_title}#{link_end} #{content}
) end end def template :invoke_result end end class BlockFloatingTitleTemplate < BaseTemplate def result(node) tag_name = "h#{node.level + 1}" id_attribute = node.id ? %( id="#{node.id}") : nil classes = [node.style, node.role].compact %(<#{tag_name}#{id_attribute} class="#{classes * ' '}">#{node.title}) end def template :invoke_result end end class BlockDlistTemplate < BaseTemplate def result(node) result_buffer = [] id_attribute = node.id ? %( id="#{node.id}") : nil case node.style when 'qanda' classes = ['qlist', node.style, node.role].compact when 'horizontal' classes = ['hdlist', node.role].compact else classes = ['dlist', node.style, node.role].compact end class_attribute = %( class="#{classes * ' '}") result_buffer << %() result_buffer << %(
#{node.title}
) if node.title? case node.style when 'qanda' result_buffer << '
    ' node.items.each do |terms, dd| result_buffer << '
  1. ' [*terms].each do |dt| result_buffer << %(

    #{dt.text}

    ) end unless dd.nil? result_buffer << %(

    #{dd.text}

    ) if dd.text? result_buffer << dd.content if dd.blocks? end result_buffer << '
  2. ' end result_buffer << '
' when 'horizontal' result_buffer << '' if (node.attr? 'labelwidth') || (node.attr? 'itemwidth') result_buffer << '' col_style_attribute = (node.attr? 'labelwidth') ? %( style="width:#{(node.attr 'labelwidth').chomp '%'}%;") : nil result_buffer << %() col_style_attribute = (node.attr? 'itemwidth') ? %( style="width:#{(node.attr 'itemwidth').chomp '%'}%;") : nil result_buffer << %() result_buffer << '' end node.items.each do |terms, dd| result_buffer << '' result_buffer << %(' result_buffer << '' result_buffer << '' end result_buffer << '
) terms_array = [*terms] last_term = terms_array.last terms_array.each do |dt| result_buffer << dt.text result_buffer << '
' if dt != last_term end result_buffer << '
' unless dd.nil? result_buffer << %(

#{dd.text}

) if dd.text? result_buffer << dd.content if dd.blocks? end result_buffer << '
' else result_buffer << '
' dt_style_attribute = node.style.nil? ? ' class="hdlist1"' : nil node.items.each do |terms, dd| [*terms].each do |dt| result_buffer << %(#{dt.text}) end unless dd.nil? result_buffer << '
' result_buffer << %(

#{dd.text}

) if dd.text? result_buffer << dd.content if dd.blocks? result_buffer << '
' end end result_buffer << '
' end result_buffer << '' result_buffer * EOL end def template :invoke_result end end class BlockListingTemplate < BaseTemplate def result(node) nowrap = (!node.document.attr? 'prewrap') || (node.option? 'nowrap') if node.style == 'source' language = node.attr 'language' language_classes = language ? %(#{language} language-#{language}) : nil case node.attr 'source-highlighter' when 'coderay' pre_class = nowrap ? ' class="CodeRay nowrap"' : ' class="CodeRay"' code_class = language ? %( class="#{language_classes}") : nil when 'pygments' pre_class = nowrap ? ' class="pygments highlight nowrap"' : ' class="pygments highlight"' code_class = language ? %( class="#{language_classes}") : nil when 'highlightjs', 'highlight.js' pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"' code_class = language ? %( class="#{language_classes}") : nil when 'prettify' pre_class = %( class="prettyprint#{nowrap ? ' nowrap' : nil}#{(node.attr? 'linenums') ? ' linenums' : nil}) pre_class = language ? %(#{pre_class} #{language_classes}") : %(#{pre_class}") code_class = nil when 'html-pipeline' pre_class = language ? %( lang="#{language}") : nil code_class = nil else pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"' code_class = language ? %( class="#{language_classes}") : nil end pre = %(#{preserve_endlines(node.content, node)}) else pre = %(#{preserve_endlines(node.content, node)}) end %(#{node.title? ? "
#{node.captioned_title}
" : nil}
#{pre}
) end def template :invoke_result end end class BlockLiteralTemplate < BaseTemplate def result(node) nowrap = (!node.document.attr? 'prewrap') || (node.option? 'nowrap') %(#{node.title? ? "
#{node.title}
" : nil}
#{preserve_endlines(node.content, node)}
) end def template :invoke_result end end class BlockAdmonitionTemplate < BaseTemplate def result(node) id = node.id name = node.attr 'name' role = node.role title = node.title? ? node.title : nil if node.document.attr? 'icons' if node.document.attr? 'icons', 'font' caption = %() else caption = %(#{node.caption}) end else caption = %(
#{node.caption}
) end %(
#{caption} #{title ? "
#{title}
" : nil} #{node.content}
) end def template :invoke_result end end class BlockParagraphTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil title_element = node.title? ? %(
#{node.title}
\n) : nil %( #{title_element}

#{node.content}

) end def template :invoke_result end end class BlockSidebarTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil title_element = node.title? ? %(
#{node.title}
\n) : nil %(
#{title_element}#{node.content}
) end def template :invoke_result end end class BlockExampleTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil title_element = node.title? ? %(
#{node.captioned_title}
\n) : nil %( #{title_element}
#{node.content}
) end def template :invoke_result end end class BlockOpenTemplate < BaseTemplate def result(node) open_block(node, node.id, node.style, node.role, node.title? ? node.title : nil, node.content) end def open_block(node, id, style, role, title, content) if style == 'abstract' if node.parent == node.document && node.document.doctype == 'book' warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.' '' else %(#{title && "
#{title}
"}
#{content}
) end elsif style == 'partintro' && (node.level != 0 || node.parent.context != :section || node.document.doctype != 'book') warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.' '' else %(#{title && "
#{title}
"}
#{content}
) end end def template :invoke_result end end class BlockPassTemplate < BaseTemplate def template :content end end class BlockQuoteTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['quoteblock', node.role].compact class_attribute = %( class="#{classes * ' '}") title_element = node.title? ? %(\n
#{node.title}
) : nil attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil if attribution || citetitle cite_element = citetitle ? %(#{citetitle}) : nil attribution_text = attribution ? %(#{citetitle ? "
\n" : nil}— #{attribution}) : nil attribution_element = %(\n
\n#{cite_element}#{attribution_text}\n
) else attribution_element = nil end %(#{title_element}
#{node.content}
#{attribution_element} ) end def template :invoke_result end end class BlockVerseTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['verseblock', node.role].compact class_attribute = %( class="#{classes * ' '}") title_element = node.title? ? %(\n
#{node.title}
) : nil attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil if attribution || citetitle cite_element = citetitle ? %(#{citetitle}) : nil attribution_text = attribution ? %(#{citetitle ? "
\n" : nil}— #{attribution}) : nil attribution_element = %(\n
\n#{cite_element}#{attribution_text}\n
) else attribution_element = nil end %(#{title_element}
#{preserve_endlines node.content, node}
#{attribution_element} ) end def template :invoke_result end end class BlockUlistTemplate < BaseTemplate def result(node) result_buffer = [] id_attribute = node.id ? %( id="#{node.id}") : nil div_classes = ['ulist', node.style, node.role].compact marker_checked = nil marker_unchecked = nil if (checklist = (node.option? 'checklist')) div_classes.insert(1, 'checklist') ul_class_attribute = ' class="checklist"' if node.option? 'interactive' marker_checked = %( ) marker_unchecked = %( ) else if node.document.attr? 'icons', 'font' marker_checked = ' ' marker_unchecked = ' ' else # could use ☑ (checked ballot) and ☐ (ballot) w/o font instead marker_checked = %( ) marker_unchecked = %( ) end end elsif !node.style.nil? ul_class_attribute = %( class="#{node.style}") else ul_class_attribute = nil end div_class_attribute = %( class="#{div_classes * ' '}") result_buffer << %() result_buffer << %(
#{node.title}
) if node.title? result_buffer << %() node.items.each do |item| if checklist && (item.attr? 'checkbox') marker = (item.attr? 'checked') ? marker_checked : marker_unchecked else marker = nil end result_buffer << '
  • ' result_buffer << %(

    #{marker}#{item.text}

    ) result_buffer << item.content if item.blocks? result_buffer << '
  • ' end result_buffer << '' result_buffer << '' result_buffer * EOL end def template :invoke_result end end class BlockOlistTemplate < BaseTemplate def result(node) result_buffer = [] id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['olist', node.style, node.role].compact class_attribute = %( class="#{classes * ' '}") result_buffer << %() result_buffer << %(
    #{node.title}
    ) if node.title? type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : nil start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : nil result_buffer << %(
      ) node.items.each do |item| result_buffer << '
    1. ' result_buffer << %(

      #{item.text}

      ) result_buffer << item.content if item.blocks? result_buffer << '
    2. ' end result_buffer << '
    ' result_buffer << '' result_buffer * EOL end def template :invoke_result end end class BlockColistTemplate < BaseTemplate def result(node) result_buffer = [] id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['colist', node.style, node.role].compact class_attribute = %( class="#{classes * ' '}") result_buffer << %() result_buffer << %(
    #{node.title}
    ) if node.title? if node.document.attr? 'icons' result_buffer << '' font_icons = node.document.attr? 'icons', 'font' node.items.each_with_index do |item, i| num = i + 1 num_element = font_icons ? %(#{num}) : %(#{num}) result_buffer << %() end result_buffer << '
    #{num_element} #{item.text}
    ' else result_buffer << '
      ' node.items.each do |item| result_buffer << %(
    1. #{item.text}

    2. ) end result_buffer << '
    ' end result_buffer << '' result_buffer * EOL end def template :invoke_result end end class BlockTableTemplate < BaseTemplate def template @template ||= @eruby.new <<-EOS <%#encoding:UTF-8%> class="tableblock frame-<%= attr :frame, 'all' %> grid-<%= attr :grid, 'all'%><%= role? ? " \#{role}" : nil %>" style="<% if !(option? 'autowidth') %>width:<%= attr :tablepcwidth %>%; <% end %><% if attr? :float %>float: <%= attr :float %>; <% end %>"><% if title? %> <%= captioned_title %><% end if (attr :rowcount) >= 0 %> <% if option? 'autowidth' @columns.each do %> <% end else @columns.each do |col| %> <% end end %> <% [:head, :foot, :body].select {|tsec| !@rows[tsec].empty? }.each do |tsec| %> ><% @rows[tsec].each do |row| %> <% row.each do |cell| %> <<%= tsec == :head ? 'th' : 'td' %> class="tableblock halign-<%= cell.attr :halign %> valign-<%= cell.attr :valign %>"#{attribute('colspan', 'cell.colspan')}#{attribute('rowspan', 'cell.rowspan')}<% cell_content = '' if tsec == :head cell_content = cell.text else case cell.style when :asciidoc cell_content = %(
    \#{cell.content}
    ) when :verse cell_content = %(
    \#{template.preserve_endlines(cell.text, self)}
    ) when :literal cell_content = %(
    \#{template.preserve_endlines(cell.text, self)}
    ) when :header cell.content.each do |text| cell_content = %(\#{cell_content}

    \#{text}

    ) end else cell.content.each do |text| cell_content = %(\#{cell_content}

    \#{text}

    ) end end end %><%= (@document.attr? 'cellbgcolor') ? %( style="background-color:\#{@document.attr 'cellbgcolor'};") : nil %>><%= cell_content %>><% end %> <% end %>
    ><% end end %> EOS end end class BlockImageTemplate < BaseTemplate def image(target, alt, title, link, node) align = (node.attr? 'align') ? (node.attr 'align') : nil float = (node.attr? 'float') ? (node.attr 'float') : nil if align || float styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact style_attribute = %( style="#{styles * ';'}") else style_attribute = nil end width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil img_element = %(#{alt}) if link img_element = %(#{img_element}) end id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['imageblock', node.style, node.role].compact class_attribute = %( class="#{classes * ' '}") title_element = title ? %(\n
    #{title}
    ) : nil %(
    #{img_element}
    #{title_element} ) end def result(node) image(node.attr('target'), node.attr('alt'), node.title? ? node.captioned_title : nil, node.attr('link'), node) end def template :invoke_result end end class BlockAudioTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['audioblock', node.style, node.role].compact class_attribute = %( class="#{classes * ' '}") title_element = node.title? ? %(\n
    #{node.captioned_title}
    ) : nil %(#{title_element}
    ) end def template :invoke_result end end class BlockVideoTemplate < BaseTemplate def result(node) id_attribute = node.id ? %( id="#{node.id}") : nil classes = ['videoblock', node.style, node.role].compact class_attribute = %( class="#{classes * ' '}") title_element = node.title? ? %(\n
    #{node.captioned_title}
    ) : nil width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil case node.attr 'poster' when 'vimeo' start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil delimiter = '?' autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil delimiter = '&' if autoplay_param loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil %(#{title_element}
    ) when 'youtube' start_param = (node.attr? 'start') ? "&start=#{node.attr 'start'}" : nil end_param = (node.attr? 'end') ? "&end=#{node.attr 'end'}" : nil autoplay_param = (node.option? 'autoplay') ? '&autoplay=1' : nil loop_param = (node.option? 'loop') ? '&loop=1' : nil controls_param = (node.option? 'nocontrols') ? '&controls=0' : nil %(#{title_element}
    ) else poster_attribute = (node.attr? 'poster') ? %( poster="#{node.media_uri(node.attr 'poster')}") : nil %(#{title_element}
    ) end end def template :invoke_result end end class BlockRulerTemplate < BaseTemplate def result(node) '
    ' end def template :invoke_result end end class BlockPageBreakTemplate < BaseTemplate def result(node) %(
    ) end def template :invoke_result end end class InlineBreakTemplate < BaseTemplate def result(node) %(#{node.text}
    \n) end def template :invoke_result end end class InlineCalloutTemplate < BaseTemplate def result(node) if node.document.attr? 'icons', 'font' %((#{node.text})) elsif node.document.attr? 'icons' src = node.icon_uri("callouts/#{node.text}") %(#{node.text}) else "(#{node.text})" end end def template :invoke_result end end class InlineQuotedTemplate < BaseTemplate NO_TAGS = [nil, nil, nil] QUOTE_TAGS = { :emphasis => ['', '', true], :strong => ['', '', true], :monospaced => ['', '', true], :superscript => ['', '', true], :subscript => ['', '', true], :double => ['“', '”', false], :single => ['‘', '’', false] } def quote_text(text, type, id, role) open, close, is_tag = QUOTE_TAGS[type] || NO_TAGS anchor = id.nil? ? nil : %() if role if is_tag quoted_text = %(#{open.chop} class="#{role}">#{text}#{close}) else quoted_text = %(#{open}#{text}#{close}) end elsif open.nil? quoted_text = text else quoted_text = %(#{open}#{text}#{close}) end anchor.nil? ? quoted_text : %(#{anchor}#{quoted_text}) end def result(node) quote_text(node.text, node.type, node.id, node.role) end def template :invoke_result end end class InlineButtonTemplate < BaseTemplate def result(node) %(#{node.text}) end def template :invoke_result end end class InlineKbdTemplate < BaseTemplate def result(node) keys = node.attr 'keys' if keys.size == 1 %(#{keys.first}) else key_combo = keys.map{|key| %(#{key}+) }.join.chop %(#{key_combo}) end end def template :invoke_result end end class InlineMenuTemplate < BaseTemplate def menu(menu, submenus, menuitem) if !submenus.empty? submenu_path = submenus.map{|submenu| %(#{submenu} ▸ ) }.join.chop %(#{menu} ▸ #{submenu_path} #{menuitem}) elsif !menuitem.nil? %(#{menu} ▸ #{menuitem}) else %(#{menu}) end end def result(node) menu(node.attr('menu'), node.attr('submenus'), node.attr('menuitem')) end def template :invoke_result end end class InlineAnchorTemplate < BaseTemplate def anchor(target, text, type, document, node) case type when :xref refid = (node.attr 'refid') || target if text.nil? # FIXME this seems like it should be prepared already text = document.references[:ids].fetch(refid, "[#{refid}]") if text.nil? end %(#{text}) when :ref %() when :link %(#{text}) when :bibref %([#{target}]) end end def result(node) anchor(node.target, node.text, node.type, node.document, node) end def template :invoke_result end end class InlineImageTemplate < BaseTemplate def image(target, type, node) if type == 'icon' && (node.document.attr? 'icons', 'font') style_class = "icon-#{target}" if node.attr? 'size' style_class = "#{style_class} icon-#{node.attr 'size'}" end if node.attr? 'rotate' style_class = "#{style_class} icon-rotate-#{node.attr 'rotate'}" end if node.attr? 'flip' style_class = "#{style_class} icon-flip-#{node.attr 'flip'}" end title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil img = %() elsif type == 'icon' && !(node.document.attr? 'icons') img = "[#{node.attr 'alt'}]" else if type == 'icon' resolved_target = node.icon_uri target else resolved_target = node.image_uri target end attrs = ['alt', 'width', 'height', 'title'].map {|name| if node.attr? name %( #{name}="#{node.attr name}") else nil end }.join img = %() end if node.attr? 'link' img = %(#{img}) end if node.role? style_classes = %(#{type} #{node.role}) else style_classes = type end style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil %(#{img}) end def result(node) image(node.target, node.type, node) end def template :invoke_result end end class InlineFootnoteTemplate < BaseTemplate def result(node) index = node.attr :index if node.type == :xref %([#{index}]) else id_attribute = node.id ? %( id="_footnote_#{node.id}") : nil %([#{index}]) end end def template :invoke_result end end class InlineIndextermTemplate < BaseTemplate def result(node) node.type == :visible ? node.text : '' end def template :invoke_result end end end # module HTML5 end # module Asciidoctor asciidoctor-0.1.4/lib/asciidoctor/block.rb000066400000000000000000000063421221220517700205130ustar00rootroot00000000000000module Asciidoctor # Public: Methods for managing blocks of Asciidoc content in a section. # # Examples # # block = Asciidoctor::Block.new(parent, :paragraph, :source => '_This_ is a ') # block.content # => "This is a <test>" class Block < AbstractBlock # Public: Create alias for context to be consistent w/ AsciiDoc alias :blockname :context # Public: Get/Set the original Array content for this block, if applicable attr_accessor :lines # Public: Initialize an Asciidoctor::Block object. # # parent - The parent AbstractBlock with a compound content model to which this Block will be appended. # context - The Symbol context name for the type of content (e.g., :paragraph). # opts - a Hash of options to customize block initialization: (default: {}) # * :content_model indicates whether blocks can be nested in this Block (:compound), otherwise # how the lines should be processed (:simple, :verbatim, :raw, :empty). (default: :simple) # * :attributes a Hash of attributes (key/value pairs) to assign to this Block. (default: {}) # * :source a String or Array of raw source for this Block. (default: nil) #-- # QUESTION should we store source_data as lines for blocks that have compound content models? def initialize(parent, context, opts = {}) super(parent, context) @content_model = opts.fetch(:content_model, nil) || :simple @attributes = opts.fetch(:attributes, nil) || {} @subs = opts[:subs] if opts.has_key? :subs raw_source = opts.fetch(:source, nil) || nil if raw_source.nil? @lines = [] elsif raw_source.class == String # FIXME make line normalization a utility method since it's used multiple times in code base!! if ::Asciidoctor::FORCE_ENCODING @lines = raw_source.lines.map {|line| "#{line.rstrip.force_encoding(::Encoding::UTF_8)}\n" } else @lines = raw_source.lines.map {|line| "#{line.rstrip}\n" } end if (last = @lines.pop) @lines.push last.chomp end else @lines = raw_source.dup end end # Public: Get an rendered version of the block content, performing # any substitutions on the content. # # Examples # # doc = Asciidoctor::Document.new # block = Asciidoctor::Block.new(doc, :paragraph, # :source => '_This_ is what happens when you a stranger in the !') # block.content # => "This is what happens when you <meet> a stranger in the <alps>!" def content case @content_model when :compound super when :simple, :verbatim, :raw apply_subs @lines.join, @subs else warn "Unknown content model '#@content_model' for block: #{to_s}" unless @content_model == :empty nil end end # Public: Returns the preprocessed source of this block # # Returns the a String containing the lines joined together or nil if there # are no lines def source @lines.join end def to_s content_summary = @content_model == :compound ? %(# of blocks = #{@blocks.size}) : %(# of lines = #{@lines.size}) %(Block[@context: :#@context, @content_model: :#@content_model, #{content_summary}]) end end end asciidoctor-0.1.4/lib/asciidoctor/callouts.rb000066400000000000000000000061351221220517700212470ustar00rootroot00000000000000module Asciidoctor # Public: Maintains a catalog of callouts and their associations. class Callouts def initialize @lists = [] @list_index = 0 next_list end # Public: Register a new callout for the given list item ordinal. # # Generates a unique id for this callout based on the index of the next callout # list in the document and the index of this callout since the end of the last # callout list. # # li_ordinal - the Integer ordinal (1-based) of the list item to which this # callout is to be associated # # Examples # # callouts = Asciidoctor::Callouts.new # callouts.register(1) # # => "CO1-1" # callouts.next_list # callouts.register(2) # # => "CO2-1" # # Returns The unique String id of this callout def register(li_ordinal) current_list << {:ordinal => li_ordinal.to_i, :id => (id = generate_next_callout_id)} @co_index += 1 id end # Public: Get the next callout index in the document # # Reads the next callout index in the document and advances the pointer. # This method is used during rendering to retrieve the unique id of the # callout that was generated during lexing. # # Returns The unique String id of the next callout in the document def read_next_id id = nil list = current_list if @co_index <= list.size id = list[@co_index - 1][:id] end @co_index += 1 id end # Public: Get a space-separated list of callout ids for the specified list item # # li_ordinal - the Integer ordinal (1-based) of the list item for which to # retrieve the callouts # # Returns A space-separated String of callout ids associated with the specified list item def callout_ids(li_ordinal) current_list.inject([]) {|collector, element| collector << element[:id] if element[:ordinal] == li_ordinal collector } * ' ' end # Public: The current list for which callouts are being collected # # Returns The Array of callouts at the position of the list index pointer def current_list @lists[@list_index - 1] end # Public: Advance to the next callout list in the document # # Returns nothing def next_list @list_index += 1 if @lists.size < @list_index @lists << [] end @co_index = 1 nil end # Public: Rewind the list index pointer, intended to be used when switching # from the parsing to rendering phase. # # Returns nothing def rewind @list_index = 1 @co_index = 1 nil end # Internal: Generate a unique id for the callout based on the internal indexes # # Returns A unique String id for this callout def generate_next_callout_id generate_callout_id(@list_index, @co_index) end # Internal: Generate a unique id for the callout at the specified position # # list_index - The 1-based Integer index of the callout list within the document # co_index - The 1-based Integer index of the callout since the end of the last callout list # # Returns A unique String id for a callout def generate_callout_id(list_index, co_index) "CO#{list_index}-#{co_index}" end end end asciidoctor-0.1.4/lib/asciidoctor/cli/000077500000000000000000000000001221220517700176365ustar00rootroot00000000000000asciidoctor-0.1.4/lib/asciidoctor/cli/invoker.rb000066400000000000000000000074521221220517700216500ustar00rootroot00000000000000module Asciidoctor module Cli # Public Invocation class for starting Asciidoctor via CLI class Invoker attr_reader :options attr_reader :documents attr_reader :code def initialize(*options) @documents = [] @out = nil @err = nil @code = 0 options = options.flatten if !options.empty? && options.first.is_a?(Cli::Options) @options = options.first elsif options.first.is_a? Hash @options = Cli::Options.new(options) else @options = Cli::Options.parse!(options) # hmmm if @options.is_a?(Integer) @code = @options @options = nil end end end def invoke! return if @options.nil? begin opts = {} profile = false infiles = [] outfile = nil tofile = nil @options.map {|k, v| case k when :input_files infiles = v when :output_file outfile = v when :destination_dir #opts[:to_dir] = File.expand_path(v) unless v.nil? opts[:to_dir] = v unless v.nil? when :attributes opts[:attributes] = v.dup when :verbose profile = true if v when :trace # currently, nothing else opts[k] = v unless v.nil? end } if infiles.size == 1 && infiles.first == '-' # allows use of block to supply stdin, particularly useful for tests inputs = [block_given? ? yield : STDIN] else inputs = infiles.map {|infile| File.new infile} end # NOTE: if infile is stdin, default to outfile as stout if outfile == '-' || (infiles.size == 1 && infiles.first == '-' && outfile.to_s.empty?) tofile = (@out || $stdout) elsif !outfile.nil? tofile = outfile opts[:mkdirs] = true else tofile = nil # automatically calculate outfile based on infile opts[:in_place] = true unless opts.has_key? :to_dir opts[:mkdirs] = true end original_opts = opts inputs.each do |input| opts = Helpers.clone_options(original_opts) if inputs.size > 1 opts[:to_file] = tofile unless tofile.nil? opts[:monitor] = {} if profile @documents ||= [] @documents.push Asciidoctor.render(input, opts) if profile monitor = opts[:monitor] err = (@err || $stderr) err.puts "Input file: #{input.respond_to?(:path) ? input.path : '-'}" err.puts " Time to read and parse source: #{'%05.5f' % monitor[:parse]}" err.puts " Time to render document: #{monitor.has_key?(:render) ? '%05.5f' % monitor[:render] : 'n/a'}" err.puts " Total time to read, parse and render: #{'%05.5f' % (monitor[:load_render] || monitor[:parse])}" end end rescue Exception => e raise e if @options[:trace] || SystemExit === e err = (@err || $stderr) err.print "#{e.class}: " if e.class != RuntimeError err.puts e.message err.puts ' Use --trace for backtrace' @code = 1 end end def document @documents.size > 0 ? @documents.first : nil end def redirect_streams(out, err = nil) @out = out @err = err end def read_output !@out.nil? ? @out.string : '' end def read_error !@err.nil? ? @err.string : '' end def reset_streams @out = nil @err = nil end end end end asciidoctor-0.1.4/lib/asciidoctor/cli/options.rb000066400000000000000000000201351221220517700216570ustar00rootroot00000000000000require 'optparse' module Asciidoctor module Cli # Public: List of options that can be specified on the command line class Options < Hash def initialize(options = {}) self[:attributes] = options[:attributes] || {} self[:input_files] = options[:input_files] || nil self[:output_file] = options[:output_file] || nil self[:safe] = options[:safe] || SafeMode::UNSAFE self[:header_footer] = options[:header_footer] || true self[:template_dirs] = options[:template_dirs] || nil self[:template_engine] = options[:template_engine] || nil if options[:doctype] self[:attributes]['doctype'] = options[:doctype] end if options[:backend] self[:attributes]['backend'] = options[:backend] end self[:eruby] = options[:eruby] || nil self[:compact] = options[:compact] || false self[:verbose] = options[:verbose] || false self[:base_dir] = options[:base_dir] self[:destination_dir] = options[:destination_dir] || nil self[:trace] = false end def self.parse!(args) Options.new.parse! args end def parse!(args) opts_parser = OptionParser.new do |opts| opts.banner = <<-EOS Usage: asciidoctor [OPTION]... FILE... Translate the AsciiDoc source FILE or FILE(s) into the backend output format (e.g., HTML 5, DocBook 4.5, etc.) By default, the output is written to a file with the basename of the source file and the appropriate extension. Example: asciidoctor -b html5 source.asciidoc EOS opts.on('-v', '--verbose', 'enable verbose mode (default: false)') do |verbose| self[:verbose] = true end opts.on('-b', '--backend BACKEND', 'set output format backend (default: html5)') do |backend| self[:attributes]['backend'] = backend end opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'manpage', 'inline'], 'document type to use when rendering output: [article, book, manpage, inline] (default: article)') do |doc_type| self[:attributes]['doctype'] = doc_type end opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file| self[:output_file] = output_file end opts.on('--safe', 'set safe mode level to safe (default: unsafe)', 'enables include macros, but restricts access to ancestor paths of source file', 'provided for compatibility with the asciidoc command') do self[:safe] = SafeMode::SAFE end opts.on('-S', '--safe-mode SAFE_MODE', ['unsafe', 'safe', 'server', 'secure'], 'set safe mode level explicitly: [unsafe, safe, server, secure] (default: unsafe)', 'disables potentially dangerous macros in source files, such as include::[]') do |safe_mode| self[:safe] = SafeMode.const_get(safe_mode.upcase) end opts.on('-s', '--no-header-footer', 'suppress output of header and footer (default: false)') do self[:header_footer] = false end opts.on('-n', '--section-numbers', 'auto-number section titles in the HTML backend; disabled by default') do self[:attributes]['numbered'] = '' end opts.on('-e', '--eruby ERUBY', ['erb', 'erubis'], 'specify eRuby implementation to render built-in templates: [erb, erubis] (default: erb)') do |eruby| self[:eruby] = eruby end opts.on('-C', '--compact', 'compact the output by removing blank lines (default: false)') do self[:compact] = true end opts.on('-a', '--attribute key[=value],key2[=value2],...', Array, 'a list of document attributes to set in the form of key, key! or key=value pair', 'unless @ is appended to the value, these attributes take precedence over attributes', 'defined in the source document') do |attribs| attribs.each do |attrib| key, val = attrib.split '=', 2 # move leading ! to end for internal processing #if val.nil? && key.start_with?('!') # key = "#{key[1..-1]}!" #end self[:attributes][key] = val || '' end end opts.on('-T', '--template-dir DIR', 'a directory containing custom render templates that override the built-in set (requires tilt gem)', 'may be specified multiple times') do |template_dir| if self[:template_dirs].nil? self[:template_dirs] = [template_dir] elsif self[:template_dirs].is_a? Array self[:template_dirs].push template_dir else self[:template_dirs] = [self[:template_dirs], template_dir] end end opts.on('-E', '--template-engine NAME', 'template engine to use for the custom render templates (loads gem on demand)') do |template_engine| self[:template_engine] = template_engine end opts.on('-B', '--base-dir DIR', 'base directory containing the document and resources (default: directory of source file)') do |base_dir| self[:base_dir] = base_dir end opts.on('-D', '--destination-dir DIR', 'destination output directory (default: directory of source file)') do |dest_dir| self[:destination_dir] = dest_dir end opts.on('--trace', 'include backtrace information on errors (default: false)') do |trace| self[:trace] = true end opts.on_tail('-h', '--help', 'show this message') do $stdout.puts opts return 0 end opts.on_tail('-V', '--version', 'display the version') do $stdout.puts "Asciidoctor #{Asciidoctor::VERSION} [http://asciidoctor.org]" return 0 end end begin infiles = [] opts_parser.parse! args if args.empty? $stderr.puts opts_parser return 1 end # shave off the file to process so that options errors appear correctly if args.size == 1 && args.first == '-' infiles.push args.pop elsif args.each do |file| if (file == '-' || file.start_with?('-')) # warn, but don't panic; we may have enough to proceed, so we won't force a failure $stderr.puts "asciidoctor: WARNING: extra arguments detected (unparsed arguments: #{args.map{|a| "'#{a}'"} * ', '}) or incorrect usage of stdin" else # TODO this glob may not be necessary as the shell should have already performed expansion matches = Dir.glob file if matches.empty? $stderr.puts "asciidoctor: FAILED: input file #{file} missing or cannot be read" return 1 end infiles.concat matches end end end infiles.each do |file| unless file == '-' || File.readable?(file) $stderr.puts "asciidoctor: FAILED: input file #{file} missing or cannot be read" return 1 end end self[:input_files] = infiles if !self[:template_dirs].nil? begin require 'tilt' rescue LoadError $stderr.puts 'asciidoctor: FAILED: tilt could not be loaded; to use a custom backend, you must have the tilt gem installed (gem install tilt)' return 1 end end rescue OptionParser::MissingArgument $stderr.puts "asciidoctor: option #{$!.message}" $stdout.puts opts_parser return 1 rescue OptionParser::InvalidOption, OptionParser::InvalidArgument $stderr.puts "asciidoctor: #{$!.message}" $stdout.puts opts_parser return 1 end self end # parse() end end end asciidoctor-0.1.4/lib/asciidoctor/debug.rb000066400000000000000000000007231221220517700205040ustar00rootroot00000000000000module Asciidoctor module Debug @show_debug = nil def self.debug warn yield if self.show_debug_output? end def self.set_debug(value) @show_debug = value end def self.show_debug_output? @show_debug || (ENV['DEBUG'] == 'true' && ENV['SUPPRESS_DEBUG'] != 'true') end def self.puts_indented(level, *args) indentation = " " * level * 2 args.each do |arg| self.debug { "#{indentation}#{arg}" } end end end end asciidoctor-0.1.4/lib/asciidoctor/document.rb000066400000000000000000000705751221220517700212500ustar00rootroot00000000000000module Asciidoctor # Public: Methods for parsing Asciidoc documents and rendering them # using erb templates. # # There are several strategies for getting the title of the document: # # doctitle - value of title attribute, if assigned and non-empty, # otherwise title of first section in document, if present # otherwise nil # name - an alias of doctitle # title - value of the title attribute, or nil if not present # first_section.title - title of first section in document, if present # header.title - title of section level 0 # # Keep in mind that you'll want to honor these document settings: # # notitle - The h1 heading should not be shown # noheader - The header block (h1 heading, author, revision info) should not be shown class Document < AbstractBlock Footnote = Struct.new(:index, :id, :text) AttributeEntry = Struct.new(:name, :value, :negate) do def initialize(name, value, negate = nil) super(name, value, negate.nil? ? value.nil? : false) end def save_to(block_attributes) (block_attributes[:attribute_entries] ||= []) << self end #def save_to_next_block(document) # (document.attributes[:pending_attribute_entries] ||= []) << self #end end # Public A read-only integer value indicating the level of security that # should be enforced while processing this document. The value must be # set in the Document constructor using the :safe option. # # A value of 0 (UNSAFE) disables any of the security features enforced # by Asciidoctor (Ruby is still subject to its own restrictions). # # A value of 1 (SAFE) closely parallels safe mode in AsciiDoc. In particular, # it prevents access to files which reside outside of the parent directory # of the source file and disables any macro other than the include macro. # # A value of 10 (SERVER) disallows the document from setting attributes that # would affect the rendering of the document, in addition to all the security # features of SafeMode::SAFE. For instance, this value disallows changing the # backend or the source-highlighter using an attribute defined in the source # document. This is the most fundamental level of security for server-side # deployments (hence the name). # # A value of 20 (SECURE) disallows the document from attempting to read files # from the file system and including the contents of them into the document, # in addition to all the security features of SafeMode::SECURE. In # particular, it disallows use of the include::[] macro and the embedding of # binary content (data uri), stylesheets and JavaScripts referenced by the # document. (Asciidoctor and trusted extensions may still be allowed to embed # trusted content into the document). # # Since Asciidoctor is aiming for wide adoption, 20 (SECURE) is the default # value and is recommended for server-side deployments. # # A value of 100 (PARANOID) is planned to disallow the use of passthrough # macros and prevents the document from setting any known attributes in # addition to all the security features of SafeMode::SECURE. Please note that # this level is not currently implemented (and therefore not enforced)! attr_reader :safe # Public: Get the Hash of document references attr_reader :references # Public: Get the Hash of document counters attr_reader :counters # Public: Get the Hash of callouts attr_reader :callouts # Public: The section level 0 block attr_reader :header # Public: Base directory for rendering this document. Defaults to directory of the source file. # If the source is a string, defaults to the current directory. attr_reader :base_dir # Public: A reference to the parent document of this nested document. attr_reader :parent_document # Public: The extensions registry attr_reader :extensions # Public: Initialize an Asciidoc object. # # data - The Array of Strings holding the Asciidoc source document. (default: []) # options - A Hash of options to control processing, such as setting the safe mode (:safe), # suppressing the header/footer (:header_footer) and attribute overrides (:attributes) # (default: {}) # # Examples # # data = File.readlines(filename) # doc = Asciidoctor::Document.new(data) # puts doc.render def initialize(data = [], options = {}) super(self, :document) if options[:parent] @parent_document = options.delete(:parent) options[:base_dir] ||= @parent_document.base_dir # QUESTION should we support setting attribute in parent document from nested document? # NOTE we must dup or else all the assignments to the overrides clobbers the real attributes @attribute_overrides = @parent_document.attributes.dup @safe = @parent_document.safe @renderer = @parent_document.renderer initialize_extensions = false @extensions = @parent_document.extensions else @parent_document = nil # 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 = (options[:attributes] || {}).inject({}) do |collector,(key,value)| if key.start_with?('!') key = key[1..-1] value = nil elsif key.end_with?('!') key = key[0..-2] value = nil end collector[key.downcase] = value collector end @safe = nil @renderer = nil initialize_extensions = Asciidoctor.const_defined?('Extensions') @extensions = nil # initialize furthur down end @header = nil @references = { :ids => {}, :footnotes => [], :links => [], :images => [], :indexterms => [], :includes => Set.new, } @counters = {} @callouts = Callouts.new @attributes_modified = Set.new @options = options unless @parent_document # safely resolve the safe mode from const, int or string if @safe.nil? && !(safe_mode = @options[:safe]) @safe = SafeMode::SECURE elsif safe_mode.is_a?(Fixnum) # be permissive in case API user wants to define new levels @safe = safe_mode else begin @safe = SafeMode.const_get(safe_mode.to_s.upcase).to_i rescue @safe = SafeMode::SECURE.to_i end end end @options[:header_footer] = @options.fetch(:header_footer, false) @attributes['encoding'] = 'UTF-8' @attributes['sectids'] = '' @attributes['notitle'] = '' unless @options[:header_footer] @attributes['toc-placement'] = 'auto' @attributes['stylesheet'] = '' @attributes['copycss'] = '' if @options[:header_footer] @attributes['prewrap'] = '' @attributes['attribute-undefined'] = COMPLIANCE[:attribute_undefined] @attributes['attribute-missing'] = COMPLIANCE[:attribute_missing] # language strings # TODO load these based on language settings @attributes['caution-caption'] = 'Caution' @attributes['important-caption'] = 'Important' @attributes['note-caption'] = 'Note' @attributes['tip-caption'] = 'Tip' @attributes['warning-caption'] = 'Warning' @attributes['appendix-caption'] = 'Appendix' @attributes['example-caption'] = 'Example' @attributes['figure-caption'] = 'Figure' #@attributes['listing-caption'] = 'Listing' @attributes['table-caption'] = 'Table' @attributes['toc-title'] = 'Table of Contents' @attributes['manname-title'] = 'NAME' @attributes['untitled-label'] = 'Untitled' @attributes['version-label'] = 'Version' @attributes['last-update-label'] = 'Last updated' @attribute_overrides['asciidoctor'] = '' @attribute_overrides['asciidoctor-version'] = VERSION safe_mode_name = SafeMode.constants.detect {|l| SafeMode.const_get(l) == @safe}.to_s.downcase @attribute_overrides['safe-mode-name'] = safe_mode_name @attribute_overrides["safe-mode-#{safe_mode_name}"] = '' @attribute_overrides['safe-mode-level'] = @safe # sync the embedded attribute w/ the value of options...do not allow override @attribute_overrides['embedded'] = @options[:header_footer] ? nil : '' # the only way to set the max-include-depth attribute is via the document options # 64 is the AsciiDoc default @attribute_overrides['max-include-depth'] ||= 64 # the only way to enable uri reads is via the document options, disabled by default unless !@attribute_overrides['allow-uri-read'].nil? @attribute_overrides['allow-uri-read'] = nil end # if the base_dir option is specified, it overrides docdir as the root for relative paths # otherwise, the base_dir is the directory of the source file (docdir) or the current # directory of the input is a string if @options[:base_dir].nil? if @attribute_overrides['docdir'] @base_dir = @attribute_overrides['docdir'] = File.expand_path(@attribute_overrides['docdir']) else #warn 'asciidoctor: WARNING: setting base_dir is recommended when working with string documents' unless nested? @base_dir = @attribute_overrides['docdir'] = File.expand_path(Dir.pwd) end else @base_dir = @attribute_overrides['docdir'] = File.expand_path(@options[:base_dir]) end # allow common attributes backend and doctype to be set using options hash unless @options[:backend].nil? @attribute_overrides['backend'] = @options[:backend].to_s end unless @options[:doctype].nil? @attribute_overrides['doctype'] = @options[:doctype].to_s end if @safe >= SafeMode::SERVER # restrict document from setting copycss, source-highlighter and backend @attribute_overrides['copycss'] ||= nil @attribute_overrides['source-highlighter'] ||= nil @attribute_overrides['backend'] ||= DEFAULT_BACKEND # restrict document from seeing the docdir and trim docfile to relative path if !@parent_document && @attribute_overrides.has_key?('docfile') @attribute_overrides['docfile'] = @attribute_overrides['docfile'][(@attribute_overrides['docdir'].length + 1)..-1] end @attribute_overrides['docdir'] = '' if @safe >= SafeMode::SECURE # assign linkcss (preventing css embedding) unless explicitly disabled from the commandline or API # effectively the same has "has key 'linkcss' and value == nil" unless @attribute_overrides.fetch('linkcss', '').nil? @attribute_overrides['linkcss'] = '' end # restrict document from enabling icons @attribute_overrides['icons'] ||= nil end end @attribute_overrides.delete_if {|key, val| verdict = false # a nil value undefines the attribute if val.nil? @attributes.delete(key) # a negative key (trailing !) undefines the attribute # NOTE already normalize above as key with nil value #elsif key.end_with? '!' # @attributes.delete(key[0..-2]) # a negative key (leading !) undefines the attribute # NOTE already normalize above as key with nil value #elsif key.start_with? '!' # @attributes.delete(key[1..-1]) # otherwise it's an attribute assignment else # a value ending in @ indicates this attribute does not override # an attribute with the same key in the document souce if val.is_a?(String) && val.end_with?('@') val = val.chop verdict = true end @attributes[key] = val end verdict } if !@parent_document # setup default backend and doctype @attributes['backend'] ||= DEFAULT_BACKEND @attributes['doctype'] ||= DEFAULT_DOCTYPE update_backend_attributes #@attributes['indir'] = @attributes['docdir'] #@attributes['infile'] = @attributes['docfile'] # dynamic intrinstic attribute values now = Time.new @attributes['localdate'] ||= now.strftime('%Y-%m-%d') @attributes['localtime'] ||= now.strftime('%H:%M:%S %Z') @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']] * ' ' # docdate, doctime and docdatetime should default to # localdate, localtime and localdatetime if not otherwise set @attributes['docdate'] ||= @attributes['localdate'] @attributes['doctime'] ||= @attributes['localtime'] @attributes['docdatetime'] ||= @attributes['localdatetime'] # fallback directories @attributes['stylesdir'] ||= '.' @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons') @extensions = initialize_extensions ? Extensions::Registry.new(self) : nil @reader = PreprocessorReader.new self, data, Asciidoctor::Reader::Cursor.new(@attributes['docfile'], @base_dir) if @extensions && @extensions.preprocessors? @extensions.load_preprocessors(self).each do |processor| @reader = processor.process(@reader, @reader.lines) || @reader end end else # 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] end # Now parse the lines in the reader into blocks Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false)) @callouts.rewind if !@parent_document && @extensions && @extensions.treeprocessors? @extensions.load_treeprocessors(self).each do |processor| processor.process end end end # Public: Get the named counter and take the next number in the sequence. # # name - the String name of the counter # seed - the initial value as a String or Integer # # returns the next number in the sequence for the specified counter def counter(name, seed = nil) if !@counters.has_key? name if seed.nil? seed = nextval(@attributes.has_key?(name) ? @attributes[name] : 0) elsif seed.to_i.to_s == seed seed = seed.to_i end @counters[name] = seed else @counters[name] = nextval(@counters[name]) end (@attributes[name] = @counters[name]) end # Public: Increment the specified counter and store it in the block's attributes # # counter_name - the String name of the counter attribute # block - the Block on which to save the counter # # returns the next number in the sequence for the specified counter def counter_increment(counter_name, block) val = counter(counter_name) AttributeEntry.new(counter_name, val).save_to(block.attributes) val end # Internal: Get the next value in the sequence. # # Handles both integer and character sequences. # # current - the value to increment as a String or Integer # # returns the next value in the sequence according to the current value's type def nextval(current) if current.is_a?(Integer) current + 1 else intval = current.to_i if intval.to_s != current.to_s (current[0].ord + 1).chr else intval + 1 end end end def register(type, value) case type when :ids if value.is_a?(Array) @references[:ids][value[0]] = (value[1] || '[' + value[0] + ']') else @references[:ids][value] = '[' + value + ']' end when :footnotes, :indexterms @references[type] << value else if @options[:catalog_assets] @references[type] << value end end end def footnotes? not @references[:footnotes].empty? end def footnotes @references[:footnotes] end def nested? @parent_document ? true : false end def embedded? # QUESTION should this be !@options[:header_footer] ? @attributes.has_key? 'embedded' end def extensions? @extensions ? true : false end # Make the raw source for the Document available. def source @reader.source if @reader end # Make the raw source lines for the Document available. def source_lines @reader.source_lines if @reader end def doctype @attributes['doctype'] end def backend @attributes['backend'] end def basebackend? base @attributes['basebackend'] == base end # The title explicitly defined in the document attributes def title @attributes['title'] end def title=(title) @header ||= Section.new(self, 0) @header.title = title end # We need to be able to return some semblance of a title def doctitle(opts = {}) if !(val = @attributes.fetch('title', '')).empty? val = title elsif !(sect = first_section).nil? && sect.title? val = sect.title else return nil end if opts[:sanitize] && val.include?('<') val.gsub(/<[^>]+>/, '').tr_s(' ', ' ').strip else val end end alias :name :doctitle # Public: Convenience method to retrieve the document attribute 'author' # # returns the full name of the author as a String def author @attributes['author'] end # Public: Convenience method to retrieve the document attribute 'revdate' # # returns the date of last revision for the document as a String def revdate @attributes['revdate'] end def notitle !@attributes.has_key?('showtitle') && @attributes.has_key?('notitle') end def noheader @attributes.has_key? 'noheader' end # QUESTION move to AbstractBlock? def first_section has_header? ? @header : (@blocks || []).detect{|e| e.is_a? Section} end def has_header? @header ? true : false end # Public: Append a content Block to this Document. # # If the child block is a Section, assign an index to it. # # block - The child Block to append to this parent Block # # Returns nothing. def <<(block) super if block.context == :section assign_index block end end # Internal: called after the header has been parsed and before the content # will be parsed. #-- # QUESTION should we invoke the Treeprocessors here, passing in a phase? # QUESTION is finalize_header the right name? def finalize_header unrooted_attributes, header_valid = true clear_playback_attributes unrooted_attributes save_attributes unrooted_attributes['invalid-header'] = true unless header_valid unrooted_attributes end # Internal: Branch the attributes so that the original state can be restored # at a future time. def save_attributes # enable toc and numbered by default in DocBook backend # NOTE the attributes_modified should go away once we have a proper attribute storage & tracking facility if @attributes['basebackend'] == 'docbook' @attributes['toc'] = '' unless attribute_locked?('toc') || @attributes_modified.include?('toc') @attributes['numbered'] = '' unless attribute_locked?('numbered') || @attributes_modified.include?('numbered') end unless @attributes.has_key?('doctitle') || (val = doctitle).nil? @attributes['doctitle'] = val end # css-signature cannot be updated after header attributes are processed if !@id && @attributes.has_key?('css-signature') @id = @attributes['css-signature'] end toc_val = @attributes['toc'] toc2_val = @attributes['toc2'] toc_position_val = @attributes['toc-position'] if (!toc_val.nil? && (toc_val != '' || toc_position_val.to_s != '')) || !toc2_val.nil? default_toc_position = 'left' default_toc_class = 'toc2' position = [toc_position_val, toc2_val, toc_val].find {|pos| pos.to_s != ''} position = default_toc_position if !position && !toc2_val.nil? @attributes['toc'] = '' case position when 'left', '<', '<' @attributes['toc-position'] = 'left' when 'right', '>', '>' @attributes['toc-position'] = 'right' when 'top', '^' @attributes['toc-position'] = 'top' when 'bottom', 'v' @attributes['toc-position'] = 'bottom' when 'center' @attributes.delete('toc2') default_toc_class = nil default_toc_position = 'center' end @attributes['toc-class'] ||= default_toc_class if default_toc_class @attributes['toc-position'] ||= default_toc_position if default_toc_position end @original_attributes = @attributes.dup # unfreeze "flexible" attributes unless nested? FLEXIBLE_ATTRIBUTES.each do |name| # turning a flexible attribute off should be permanent # (we may need more config if that's not always the case) if @attribute_overrides.has_key?(name) && !@attribute_overrides[name].nil? @attribute_overrides.delete(name) end end end end # Internal: Restore the attributes to the previously saved state def restore_attributes @attributes = @original_attributes end # Internal: Delete any attributes stored for playback def clear_playback_attributes(attributes) attributes.delete(:attribute_entries) end # Internal: Replay attribute assignments at the block level def playback_attributes(block_attributes) if block_attributes.has_key? :attribute_entries block_attributes[:attribute_entries].each do |entry| if entry.negate @attributes.delete(entry.name) else @attributes[entry.name] = entry.value end end end end # Public: Set the specified attribute on the document if the name is not locked # # If the attribute is locked, false is returned. Otherwise, the value is # assigned to the attribute name after first performing attribute # substitutions on the value. If the attribute name is 'backend', then the # value of backend-related attributes are updated. # # name - the String attribute name # value - the String attribute value # # returns true if the attribute was set, false if it was not set because it's locked def set_attribute(name, value) if attribute_locked?(name) false else @attributes[name] = apply_attribute_value_subs(value) @attributes_modified << name if name == 'backend' update_backend_attributes() end true end end # Public: Delete the specified attribute from the document if the name is not locked # # If the attribute is locked, false is returned. Otherwise, the attribute is deleted. # # name - the String attribute name # # returns true if the attribute was deleted, false if it was not because it's locked def delete_attribute(name) if attribute_locked?(name) false else @attributes.delete(name) @attributes_modified << name true end end # Public: Determine if the attribute has been locked by being assigned in document options # # key - The attribute key to check # # Returns true if the attribute is locked, false otherwise def attribute_locked?(name) @attribute_overrides.has_key?(name) end # Internal: Apply substitutions to the attribute value # # If the value is an inline passthrough macro (e.g., pass:[text]), then # apply the substitutions defined on the macro to the text. Otherwise, # apply the verbatim substitutions to the value. # # value - The String attribute value on which to perform substitutions # # Returns The String value with substitutions performed. def apply_attribute_value_subs(value) if value.match(REGEXP[:pass_macro_basic]) # copy match for Ruby 1.8.7 compat m = $~ if !m[1].empty? subs = resolve_pass_subs m[1] subs.empty? ? m[2] : apply_subs(m[2], subs) else m[2] end else apply_header_subs(value) end end # Public: Update the backend attributes to reflect a change in the selected backend def update_backend_attributes() backend = @attributes['backend'] if BACKEND_ALIASES.has_key? backend backend = @attributes['backend'] = BACKEND_ALIASES[backend] end basebackend = backend.sub(REGEXP[:trailing_digit], '') page_width = DEFAULT_PAGE_WIDTHS[basebackend] if page_width @attributes['pagewidth'] = page_width else @attributes.delete('pagewidth') end @attributes["backend-#{backend}"] = '' @attributes['basebackend'] = basebackend @attributes["basebackend-#{basebackend}"] = '' # REVIEW cases for the next two assignments @attributes["#{backend}-#{@attributes['doctype']}"] = '' @attributes["#{basebackend}-#{@attributes['doctype']}"] = '' ext = DEFAULT_EXTENSIONS[basebackend] || '.html' @attributes['outfilesuffix'] = ext file_type = ext[1..-1] @attributes['filetype'] = file_type @attributes["filetype-#{file_type}"] = '' end def renderer(opts = {}) return @renderer if @renderer render_options = {} # Load up relevant Document @options if @options.has_key? :template_dir render_options[:template_dirs] = [@options[:template_dir]] elsif @options.has_key? :template_dirs render_options[:template_dirs] = @options[:template_dirs] end render_options[:template_cache] = @options.fetch(:template_cache, true) render_options[:backend] = @attributes.fetch('backend', 'html5') render_options[:template_engine] = @options[:template_engine] render_options[:eruby] = @options.fetch(:eruby, 'erb') render_options[:compact] = @options.fetch(:compact, false) # Override Document @option settings with options passed in render_options.merge! opts @renderer = Renderer.new(render_options) end # Public: Render the Asciidoc document using the templates # loaded by Renderer. If a :template_dir is not specified, # or a template is missing, the renderer will fall back to # using the appropriate built-in template. def render(opts = {}) restore_attributes r = renderer(opts) # QUESTION should we add Preserializeprocessors? is it the right name? #if !@parent_document && @extensions && @extensions.preserializeprocessors? # @extensions.load_preserializeprocessors(self).each do |processor| # processor.process r # end #end if doctype == 'inline' # QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph? if !(block = @blocks.first).nil? && block.content_model != :compound output = block.content else output = '' end else output = @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self) end if !@parent_document && @extensions if @extensions.postprocessors? @extensions.load_postprocessors(self).each do |processor| output = processor.process output end end @extensions.reset end output end def content # per AsciiDoc-spec, remove the title before rendering the body, # regardless of whether the header is rendered) @attributes.delete('title') super end # Public: Read the docinfo file(s) for inclusion in the # document template # # If the docinfo1 attribute is set, read the docinfo.ext file. If the docinfo # attribute is set, read the doc-name.docinfo.ext file. If the docinfo2 # attribute is set, read both files in that order. # # pos - The Symbol position of the docinfo, either :header or :footer. (default: :header) # ext - The extension of the docinfo file(s). If not set, the extension # will be determined based on the basebackend. (default: nil) # # returns The contents of the docinfo file(s) def docinfo(pos = :header, ext = nil) if safe >= SafeMode::SECURE '' else case pos when :footer qualifier = '-footer' else qualifier = nil end ext = @attributes['outfilesuffix'] if ext.nil? content = nil docinfo = @attributes.has_key?('docinfo') docinfo1 = @attributes.has_key?('docinfo1') docinfo2 = @attributes.has_key?('docinfo2') docinfo_filename = "docinfo#{qualifier}#{ext}" if docinfo1 || docinfo2 docinfo_path = normalize_system_path(docinfo_filename) content = read_asset(docinfo_path) content = sub_attributes(content.lines.entries).join unless content.nil? end if (docinfo || docinfo2) && @attributes.has_key?('docname') docinfo_path = normalize_system_path("#{@attributes['docname']}-#{docinfo_filename}") content2 = read_asset(docinfo_path) unless content2.nil? content2 = sub_attributes(content2.lines.entries).join content = content.nil? ? content2 : "#{content}\n#{content2}" end end # to_s forces nil to empty string content.to_s end end def to_s %[#{super.to_s} - #{doctitle}] end end end asciidoctor-0.1.4/lib/asciidoctor/extensions.rb000066400000000000000000000275561221220517700216320ustar00rootroot00000000000000module Asciidoctor module Extensions class Extension class << self def register ::Asciidoctor::Extensions.register self end def activate registry, document end end end class << self def registered? !@registered.nil? end def registered @registered ||= [] end # QUESTION should we require extensions to have names? # how about autogenerate name for class, assume extension # is name of block if block is given # having a name makes it easier to unregister an extension def register extension = nil, &block if block_given? registered << block elsif extension registered << resolve_class(extension) end end def resolve_class(object) object.is_a?(Class) ? object : class_for_name(object.to_s) end def class_for_name(qualified_name) qualified_name.split('::').inject(Object) do |module_, name| if name.empty? module_ elsif module_.const_defined? name module_.const_get(name) else raise "Could not resolve class for name: #{qualified_name}" end end end def unregister_all @registered = [] end end class Registry attr_accessor :preprocessors attr_accessor :treeprocessors attr_accessor :postprocessors attr_accessor :include_processors attr_accessor :blocks attr_accessor :block_macros attr_accessor :inline_macros def initialize document = nil @preprocessors = [] @treeprocessors = [] @postprocessors = [] @include_processors = [] @include_processor_cache = {} @block_delimiters = {} @blocks = {} @block_processor_cache = {} @block_macros = {} @block_macro_processor_cache = {} @inline_macros = {} @inline_macro_processor_cache = {} Extensions.registered.each do |extension| if extension.is_a? Proc register document, &extension else extension.activate self, document end end end def preprocessor processor, position = :<< processor = resolve_processor_class processor if position == :<< || @preprocessors.empty? @preprocessors.push processor elsif position == :>> @preprocessors.unshift processor else @preprocessors.push processor end end def preprocessors? !@preprocessors.empty? end def load_preprocessors *args @preprocessors.map do |processor| processor.new(*args) end end def treeprocessor processor, position = :<< processor = resolve_processor_class processor if position == :<< || @treeprocessors.empty? @treeprocessors.push processor elsif position == :>> @treeprocessors.unshift processor else @treeprocessors.push processor end end def treeprocessors? !@treeprocessors.empty? end def load_treeprocessors *args @treeprocessors.map do |processor| processor.new(*args) end end def postprocessor processor, position = :<< processor = resolve_processor_class processor if position == :<< || @postprocessors.empty? @postprocessors.push processor elsif position == :>> @postprocessors.unshift processor else @postprocessors.push processor end end def postprocessors? !@postprocessors.empty? end def load_postprocessors *args @postprocessors.map do |processor| processor.new(*args) end end def include_processor processor, position = :<< processor = resolve_processor_class processor if position == :<< || @include_processors.empty? @include_processors.push processor elsif position == :>> @include_processors.unshift processor else @include_processors.push processor end end def include_processors? !@include_processors.empty? end def load_include_processors *args @include_processors.map do |processor| processor.new(*args) end # QUESTION do we need/want the cache? #@include_processors.map do |processor| # @include_processor_cache[processor] ||= processor.new(*args) #end end # TODO allow contexts to be specified here, perhaps as [:upper, [:paragraph, :sidebar]] def block name, processor, delimiter = nil, &block processor = resolve_processor_class processor @blocks[name] = processor if block_given? @block_delimiters[block] = name elsif delimiter && delimiter.is_a?(Regexp) @block_delimiters[delimiter] = name end end def blocks? !@blocks.empty? end def block_delimiters? !@block_delimiters.empty? end # NOTE block delimiters not yet implemented def at_block_delimiter? line @block_delimiters.each do |delimiter, name| if delimiter.is_a? Proc if delimiter.call(line) return name end else if line.match(delimiter) return name end end end false end def load_block_processor name, *args @block_processor_cache[name] ||= @blocks[name].new(name.to_sym, *args) end def processor_registered_for_block? name, context if @blocks.has_key? name.to_sym (@blocks[name.to_sym].config.fetch(:contexts, nil) || []).include?(context) else false end end def block_macro name, processor processor = resolve_processor_class processor @block_macros[name.to_s] = processor end def block_macros? !@block_macros.empty? end def load_block_macro_processor name, *args @block_macro_processor_cache[name] ||= @block_macros[name].new(name, *args) end def processor_registered_for_block_macro? name @block_macros.has_key? name end # TODO probably need ordering control before/after other inline macros def inline_macro name, processor processor = resolve_processor_class processor @inline_macros[name.to_s] = processor end def inline_macros? !@inline_macros.empty? end def load_inline_macro_processor name, *args @inline_macro_processor_cache[name] ||= @inline_macros[name].new(name, *args) end def load_inline_macro_processors *args @inline_macros.map do |name, processor| load_inline_macro_processor name, *args end end def processor_registered_for_inline_macro? name @inline_macros.has_key? name end def register document, &block instance_exec document, &block end def resolve_processor_class object ::Asciidoctor::Extensions.resolve_class object end def reset @block_processor_cache = {} @block_macro_processor_cache = {} @inline_macro_processor_cache = {} end end class Processor def initialize(document) @document = document end end # Public: Preprocessors are run after the source text is split into lines and # before parsing begins. # # Prior to invoking the preprocessor, Asciidoctor splits the source text into # lines and normalizes them. The normalize process strips trailing whitespace # from each line and leaves behind a line-feed character (i.e., "\n"). # # Asciidoctor passes a reference to the Reader and a copy of the lines Array # to the process method of an instance of each registered Preprocessor. The # Preprocessor modifies the Array as necessary and either returns a reference # to the same Reader or a reference to a new one. # # Preprocessors must extend Asciidoctor::Extensions::Preprocessor. class Preprocessor < Processor # Public: Accepts the Reader and an Array of lines, modifies them as # needed, then returns the Reader or a reference to a new one. # # Each subclass of Preprocessor should override this method. def process reader, lines reader end end # Public: Treeprocessors are run on the Document after the source has been # parsed into an abstract syntax tree, as represented by the Document object # and its child Node objects. # # Asciidoctor invokes the process method on an instance of each registered # Treeprocessor. # # QUESTION should the treeprocessor get invoked after parse header too? # # Treeprocessors must extend Asciidoctor::Extensions::Treeprocessor. class Treeprocessor < Processor def process end end # Public: Postprocessors are run after the document is rendered and before # it's written to the output stream. # # Asciidoctor passes a reference to the output String to the process method # of each registered Postprocessor. The Preprocessor modifies the String as # necessary and returns the String replacement. # # The markup format in the String is determined from the backend used to # render the Document. The backend and be looked up using the backend method # on the Document object, as well as various backend-related document # attributes. # # Postprocessors can also be used to relocate assets needed by the published # document. # # Postprocessors must extend Asciidoctor::Extensions::Postprocessor. class Postprocessor < Processor def process output output end end # Public: IncludeProcessors are used to process include::[] macros in the # source document. # # When Asciidoctor discovers an include::[] macro in the source document, it # iterates through the IncludeProcessors and delegates the work of reading # the content to the first processor that identifies itself as capable of # handling that target. # # IncludeProcessors must extend Asciidoctor::Extensions::IncludeProcessor. class IncludeProcessor < Processor def process target, attributes output end end # Supported options: # * :contexts - The blocks contexts (types) on which this style can be used (default: [:paragraph, :open] # * :content_model - The structure of the content supported in this block (default: :compound) # * :pos_attrs - A list of attribute names used to map positional attributes (default: nil) # * :default_attrs - Set default values for attributes (default: nil) # * ... class BlockProcessor < Processor class << self def config @config ||= {:contexts => [:paragraph, :open]} end def option(key, default_value) config[key] = default_value end end attr_reader :document attr_reader :context attr_reader :options def initialize(context, document, opts = {}) super(document) @context = context @options = self.class.config.dup opts.delete(:contexts) # contexts can't be overridden @options.update(opts) #@options[:contexts] ||= [:paragraph, :open] @options[:content_model] ||= :compound end def process parent, reader, attributes nil end end class MacroProcessor < Processor class << self def config @config ||= {} end def option(key, default_value) config[key] = default_value end end attr_reader :document attr_reader :name attr_reader :options def initialize(name, document, opts = {}) super(document) @name = name @options = self.class.config.dup @options.update(opts) end def process parent, target, attributes, source = nil nil end end class BlockMacroProcessor < MacroProcessor end # TODO break this out into different pattern types # for example, FormalInlineMacro, ShortInlineMacro (no target) and other patterns class InlineMacroProcessor < MacroProcessor def initialize(name, document, opts = {}) super @regexp = nil end def regexp if @options[:short_form] @regexp ||= %r(\\?#{@name}:\[((?:\\\]|[^\]])*?)\]) else @regexp ||= %r(\\?#{@name}:(\S+?)\[((?:\\\]|[^\]])*?)\]) end end end end end asciidoctor-0.1.4/lib/asciidoctor/helpers.rb000066400000000000000000000046111221220517700210600ustar00rootroot00000000000000module Asciidoctor module Helpers # Internal: Prior to invoking Kernel#require, issues a warning urging a # manual require if running in a threaded environment. # # name - the String name of the library to require. # # returns false if the library is detected on the load path or the return # value of delegating to Kernel#require def self.require_library(name, gem_name = nil) if Thread.list.size > 1 main_script = "#{name}.rb" main_script_path_segment = "/#{name}.rb" if !$LOADED_FEATURES.detect {|p| p == main_script || p.end_with?(main_script_path_segment) }.nil? return false else warn "WARN: asciidoctor is autoloading '#{name}' in threaded environment. " + "The use of an explicit require '#{name}' statement is recommended." end end begin require name rescue LoadError => e if gem_name fail "asciidoctor: FAILED: required gem '#{gem_name === true ? name : gem_name}' is not installed. Processing aborted." else fail "asciidoctor: FAILED: #{e.chomp '.'}. Processing aborted." end end end # Public: Encode a string for inclusion in a URI # # str - the string to encode # # returns an encoded version of the str def self.encode_uri(str) str.gsub(REGEXP[:uri_encode_chars]) do match = $& buf = '' match.each_byte do |c| buf << sprintf('%%%02X', c) end buf end end # Public: Removes the file extension from filename and returns the result # # file_name - The String file name to process # # Examples # # Helpers.rootname('part1/chapter1.adoc') # # => "part1/chapter1" # # Returns the String filename with the file extension removed def self.rootname(file_name) ext = File.extname(file_name) if ext.empty? file_name else file_name[0...-ext.length] end end def self.mkdir_p(dir) unless File.directory? dir parent_dir = File.dirname(dir) if !File.directory?(parent_dir = File.dirname(dir)) && parent_dir != '.' mkdir_p(parent_dir) end Dir.mkdir(dir) end end # Public: Create a copy of options such that no references are shared # returns A deep clone of the options Hash def self.clone_options(opts) clone = opts.dup if opts.has_key? :attributes clone[:attributes] = opts[:attributes].dup end clone end end end asciidoctor-0.1.4/lib/asciidoctor/inline.rb000066400000000000000000000016131221220517700206730ustar00rootroot00000000000000module Asciidoctor # Public: Methods for managing inline elements in AsciiDoc block class Inline < AbstractNode # Public: Get/Set the String name of the render template attr_accessor :template_name # Public: Get the text of this inline element attr_reader :text # Public: Get the type (qualifier) of this inline element attr_reader :type # Public: Get/Set the target (e.g., uri) of this inline element attr_accessor :target def initialize(parent, context, text = nil, opts = {}) super(parent, context) @template_name = "inline_#{context}" @text = text @id = opts[:id] @type = opts[:type] @target = opts[:target] if opts.has_key?(:attributes) && (attributes = opts[:attributes]).is_a?(Hash) update_attributes(opts[:attributes]) unless attributes.empty? end end def render renderer.render(@template_name, self).chomp end end end asciidoctor-0.1.4/lib/asciidoctor/lexer.rb000066400000000000000000002766671221220517700205630ustar00rootroot00000000000000module Asciidoctor # Public: Methods to parse lines of AsciiDoc into an object hierarchy # representing the structure of the document. All methods are class methods and # should be invoked from the Lexer class. The main entry point is ::next_block. # No Lexer instances shall be discovered running around. (Any attempt to # instantiate a Lexer will be futile). # # The object hierarchy created by the Lexer consists of zero or more Section # and Block objects. Section objects may be nested and a Section object # contains zero or more Block objects. Block objects may be nested, but may # only contain other Block objects. Block objects which represent lists may # contain zero or more ListItem objects. # # Examples # # # Create a Reader for the AsciiDoc lines and retrieve the next block from it. # # Lexer::next_block requires a parent, so we begin by instantiating an empty Document. # # doc = Document.new # reader = Reader.new lines # block = Lexer.next_block(reader, doc) # block.class # # => Asciidoctor::Block class Lexer BlockMatchData = Struct.new(:context, :masq, :tip, :terminator) # Public: Make sure the Lexer object doesn't get initialized. # # Raises RuntimeError if this constructor is invoked. def initialize raise 'Au contraire, mon frere. No lexer instances will be running around.' end # Public: Parses AsciiDoc source read from the Reader into the Document # # This method is the main entry-point into the Lexer when parsing a full document. # It first looks for and, if found, processes the document title. It then # proceeds to iterate through the lines in the Reader, parsing the document # into nested Sections and Blocks. # # reader - the Reader holding the source lines of the document # document - the empty Document into which the lines will be parsed # options - a Hash of options to control processing # # returns the Document object def self.parse(reader, document, options = {}) block_attributes = parse_document_header(reader, document) unless options[:header_only] while reader.has_more_lines? new_section, block_attributes = next_section(reader, document, block_attributes) document << new_section unless new_section.nil? end end document end # Public: Parses the document header of the AsciiDoc source read from the Reader # # Reads the AsciiDoc source from the Reader until the end of the document # header is reached. The Document object is populated with information from # the header (document title, document attributes, etc). The document # attributes are then saved to establish a save point to which to rollback # after parsing is complete. # # This method assumes that there are no blank lines at the start of the document, # which are automatically removed by the reader. # # returns the Hash of orphan block attributes captured above the header def self.parse_document_header(reader, document) # capture any lines of block-level metadata and plow away any comment lines # that precede first block block_attributes = parse_block_metadata_lines(reader, document) # special case, block title is not allowed above document title, # carry attributes over to the document body if block_attributes.has_key?('title') return document.finalize_header block_attributes, false end # yep, document title logic in AsciiDoc is just insanity # definitely an area for spec refinement assigned_doctitle = nil unless (val = document.attributes.fetch('doctitle', '')).empty? document.title = val assigned_doctitle = val end section_title = nil # check if the first line is the document title # if so, add a header to the document and parse the header metadata if is_next_line_document_title?(reader, block_attributes) document.id, doctitle, _, _ = parse_section_title(reader, document) unless assigned_doctitle document.title = doctitle assigned_doctitle = doctitle end document.attributes['doctitle'] = section_title = doctitle # QUESTION: should the id assignment on Document be encapsulated in the Document class? if document.id.nil? && block_attributes.has_key?('id') document.id = block_attributes.delete('id') end parse_header_metadata(reader, document) end if !(val = document.attributes.fetch('doctitle', '')).empty? && val != section_title document.title = val assigned_doctitle = val end # restore doctitle attribute to original assignment if assigned_doctitle document.attributes['doctitle'] = assigned_doctitle end # parse title and consume name section of manpage document parse_manpage_header(reader, document) if document.doctype == 'manpage' # NOTE block_attributes are the block-level attributes (not document attributes) that # precede the first line of content (document title, first section or first block) document.finalize_header block_attributes end # Public: Parses the manpage header of the AsciiDoc source read from the Reader # # returns Nothing def self.parse_manpage_header(reader, document) if (m = document.attributes['doctitle'].match(REGEXP[:mantitle_manvolnum])) document.attributes['mantitle'] = document.sub_attributes(m[1].rstrip.downcase) document.attributes['manvolnum'] = m[2].strip else warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed manpage title" end reader.skip_blank_lines if is_next_line_section?(reader, {}) name_section = initialize_section(reader, document, {}) if name_section.level == 1 name_section_buffer = reader.read_lines_until(:break_on_blank_lines => true).join.tr_s("\n ", ' ') if (m = name_section_buffer.match(REGEXP[:manname_manpurpose])) document.attributes['manname'] = m[1] document.attributes['manpurpose'] = m[2] # TODO parse multiple man names if document.backend == 'manpage' document.attributes['docname'] = document.attributes['manname'] document.attributes['outfilesuffix'] = ".#{document.attributes['manvolnum']}" end else warn "asciidoctor: ERROR: #{reader.prev_line_info}: malformed name section body" end else warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section title must be at level 1" end else warn "asciidoctor: ERROR: #{reader.prev_line_info}: name section expected" end end # Public: Return the next section from the Reader. # # This method process block metadata, content and subsections for this # section and returns the Section object and any orphaned attributes. # # If the parent is a Document and has a header (document title), then # this method will put any non-section blocks at the start of document # into a preamble Block. If there are no such blocks, the preamble is # dropped. # # Since we are reading line-by-line, there's a chance that metadata # that should be associated with the following block gets consumed. # To deal with this case, the method returns a running Hash of # "orphaned" attributes that get passed to the next Section or Block. # # reader - the source Reader # parent - the parent Section or Document of this new section # attributes - a Hash of metadata that was left orphaned from the # previous Section. # # Examples # # source # # => "Greetings\n---------\nThis is my doc.\n\nSalutations\n-----------\nIt is awesome." # # reader = Reader.new source.lines.entries # # create empty document to parent the section # # and hold attributes extracted from header # doc = Document.new # # Lexer.next_section(reader, doc).first.title # # => "Greetings" # # Lexer.next_section(reader, doc).first.title # # => "Salutations" # # returns a two-element Array containing the Section and Hash of orphaned attributes def self.next_section(reader, parent, attributes = {}) preamble = false # FIXME if attributes[1] is a verbatim style, then don't check for section # check if we are at the start of processing the document # NOTE we could drop a hint in the attributes to indicate # that we are at a section title (so we don't have to check) if parent.is_a?(Document) && parent.blocks.empty? && (parent.has_header? || attributes.delete('invalid-header') || !is_next_line_section?(reader, attributes)) if parent.has_header? preamble = Block.new(parent, :preamble, :content_model => :compound) parent << preamble end section = parent current_level = 0 if parent.attributes.has_key? 'fragment' expected_next_levels = nil # small tweak to allow subsequent level-0 sections for book doctype elsif parent.doctype == 'book' expected_next_levels = [0, 1] else expected_next_levels = [1] end else section = initialize_section(reader, parent, attributes) # clear attributes, except for title which carries over # section title to next block of content attributes = attributes.delete_if {|k, v| k != 'title'} current_level = section.level # subsections in preface & appendix in multipart books start at level 2 if current_level == 0 && section.special && section.document.doctype == 'book' && ['preface', 'appendix'].include?(section.sectname) expected_next_levels = [current_level + 2] else expected_next_levels = [current_level + 1] end end reader.skip_blank_lines # Parse lines belonging to this section and its subsections until we # reach the end of this section level # # 1. first look for metadata thingies (anchor, attribute list, block title line, etc) # 2. then look for a section, recurse if found # 3. then process blocks # # We have to parse all the metadata lines before continuing with the loop, # otherwise subsequent metadata lines get interpreted as block content while reader.has_more_lines? parse_block_metadata_lines(reader, section, attributes) next_level = is_next_line_section? reader, attributes if next_level next_level += section.document.attr('leveloffset', 0).to_i doctype = parent.document.doctype if next_level > current_level || (section.is_a?(Document) && next_level == 0) if next_level == 0 && doctype != 'book' warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections" elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level) warn "asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: " + "expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, " + "got level #{next_level}" end # the attributes returned are those that are orphaned new_section, attributes = next_section(reader, section, attributes) section << new_section else if next_level == 0 && doctype != 'book' warn "asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections" end # close this section (and break out of the nesting) to begin a new one break end else # just take one block or else we run the risk of overrunning section boundaries new_block = next_block(reader, (preamble || section), attributes, :parse_metadata => false) if !new_block.nil? (preamble || section) << new_block attributes = {} else # don't clear attributes if we don't find a block because they may # be trailing attributes that didn't get associated with a block end end reader.skip_blank_lines end if preamble && !preamble.blocks? # drop the preamble if it has no content section.blocks.delete_at(0) end # The attributes returned here are orphaned attributes that fall at the end # of a section that need to get transfered to the next section # see "trailing block attributes transfer to the following section" in # test/attributes_test.rb for an example [section != parent ? section : nil, attributes.dup] end # Public: Return the next Section or Block object from the Reader. # # Begins by skipping over blank lines to find the start of the next Section # or Block. Processes each line of the reader in sequence until a Section or # Block is found or the reader has no more lines. # # Uses regular expressions from the Asciidoctor module to match Section # and Block delimiters. The ensuing lines are then processed according # to the type of content. # # reader - The Reader from which to retrieve the next block # parent - The Document, Section or Block to which the next block belongs # # Returns a Section or Block object holding the parsed content of the processed lines #-- # QUESTION should next_block have an option for whether it should keep looking until # a block is found? right now it bails when it encounters a line to be skipped def self.next_block(reader, parent, attributes = {}, options = {}) # Skip ahead to the block content skipped = reader.skip_blank_lines # bail if we've reached the end of the parent block or document return nil unless reader.has_more_lines? text_only = options[:text] # check for option to find list item text only # if skipped a line, assume a list continuation was # used and block content is acceptable if text_only && skipped > 0 options.delete(:text) text_only = false end parse_metadata = options.fetch(:parse_metadata, true) #parse_sections = options.fetch(:parse_sections, false) document = parent.document if (extensions = document.extensions) block_extensions = extensions.blocks? macro_extensions = extensions.block_macros? else block_extensions = macro_extensions = false end #parent_context = parent.is_a?(Block) ? parent.context : nil in_list = parent.is_a?(List) block = nil style = nil explicit_style = nil while reader.has_more_lines? && block.nil? # if parsing metadata, read until there is no more to read if parse_metadata && parse_block_metadata_line(reader, document, attributes, options) reader.advance next #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes) # block, attributes = next_section(reader, parent, attributes) # break end # QUESTION should we introduce a parsing context object? this_line = reader.read_line delimited_block = false block_context = nil cloaked_context = nil terminator = nil # QUESTION put this inside call to rekey attributes? if attributes[1] style, explicit_style = parse_style_attribute(attributes, reader) end if delimited_blk_match = is_delimited_block?(this_line, true) delimited_block = true block_context = cloaked_context = delimited_blk_match.context terminator = delimited_blk_match.terminator if !style style = attributes['style'] = block_context.to_s elsif style != block_context.to_s if delimited_blk_match.masq.include? style block_context = style.to_sym elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style) block_context = :admonition elsif block_extensions && extensions.processor_registered_for_block?(style, block_context) block_context = style.to_sym else warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style}" style = block_context.to_s end end end if !delimited_block # this loop only executes once; used for flow control # break once a block is found or at end of loop # returns nil if the line must be dropped # Implementation note - while(true) is twice as fast as loop while true # process lines verbatim if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style) block_context = style.to_sym reader.unshift_line this_line # advance to block parsing => break end # process lines normally unless text_only first_char = Compliance.markdown_syntax ? this_line.lstrip[0..0] : this_line[0..0] # NOTE we're letting break lines (ruler, page_break, etc) have attributes if BREAK_LINES.has_key?(first_char) && this_line.length > 3 && (match = this_line.match(Compliance.markdown_syntax ? REGEXP[:break_line_plus] : REGEXP[:break_line])) block = Block.new(parent, BREAK_LINES[first_char], :content_model => :empty) break elsif (match = this_line.match(REGEXP[:media_blk_macro])) blk_ctx = match[1].to_sym block = Block.new(parent, blk_ctx, :content_model => :empty) if blk_ctx == :image posattrs = ['alt', 'width', 'height'] elsif blk_ctx == :video posattrs = ['poster', 'width', 'height'] else posattrs = [] end unless style.nil? || explicit_style attributes['alt'] = style if blk_ctx == :image attributes.delete('style') style = nil end block.parse_attributes(match[3], posattrs, :unescape_input => (blk_ctx == :image), :sub_input => true, :sub_result => false, :into => attributes) target = block.sub_attributes(match[2], :attribute_missing => 'drop-line') if target.empty? if document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip' # retain as unparsed return Block.new(parent, :paragraph, :source => [this_line.chomp]) else # drop the line if target resolves to nothing return nil end end attributes['target'] = target block.title = attributes.delete('title') if attributes.has_key?('title') if blk_ctx == :image document.register(:images, target) attributes['alt'] ||= File.basename(target, File.extname(target)).tr('_-', ' ') # QUESTION should video or audio have an auto-numbered caption? block.assign_caption attributes.delete('caption'), 'figure' end break # NOTE we're letting the toc macro have attributes elsif first_char == 't' && (match = this_line.match(REGEXP[:toc])) block = Block.new(parent, :toc, :content_model => :empty) block.parse_attributes(match[1], [], :sub_result => false, :into => attributes) break elsif macro_extensions && (match = this_line.match(REGEXP[:generic_blk_macro])) && extensions.processor_registered_for_block_macro?(match[1]) name = match[1] target = match[2] raw_attributes = match[3] processor = extensions.load_block_macro_processor name, document unless raw_attributes.empty? document.parse_attributes(raw_attributes, processor.options.fetch(:pos_attrs, []), :sub_input => true, :sub_result => false, :into => attributes) end if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty? default_attrs.each {|k, v| attributes[k] ||= v } end block = processor.process parent, target, attributes return nil if block.nil? break end end # haven't found anything yet, continue if (match = this_line.match(REGEXP[:colist])) block = List.new(parent, :colist) attributes['style'] = 'arabic' reader.unshift_line this_line expected_index = 1 begin # might want to move this check to a validate method if match[1].to_i != expected_index # FIXME this lineno - 2 hack means we need a proper look-behind cursor warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: callout list item index: expected #{expected_index} got #{match[1]}" end list_item = next_list_item(reader, block, match) expected_index += 1 if !list_item.nil? block << list_item coids = document.callouts.callout_ids(block.items.size) if !coids.empty? list_item.attributes['coids'] = coids else # FIXME this lineno - 2 hack means we need a proper look-behind cursor warn "asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size}" end end end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist]) document.callouts.next_list break elsif (match = this_line.match(REGEXP[:ulist])) reader.unshift_line this_line block = next_outline_list(reader, :ulist, parent) break elsif (match = this_line.match(REGEXP[:olist])) reader.unshift_line this_line block = next_outline_list(reader, :olist, parent) # QUESTION move this logic to next_outline_list? if !attributes['style'] && !block.attributes['style'] marker = block.items.first.marker if marker.start_with? '.' # first one makes more sense, but second one is AsciiDoc-compliant #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s else style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) } attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s end end break elsif (match = this_line.match(REGEXP[:dlist])) reader.unshift_line this_line block = next_labeled_list(reader, match, parent) break elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil)) reader.unshift_line this_line float_id, float_title, float_level, _ = parse_section_title(reader, document) float_id ||= attributes['id'] if attributes.has_key?('id') block = Block.new(parent, :floating_title, :content_model => :empty) if float_id.nil? || float_id.empty? # FIXME remove hack of creating throwaway Section to get at the generate_id method tmp_sect = Section.new(parent) tmp_sect.title = float_title block.id = tmp_sect.generate_id else block.id = float_id end document.register(:ids, [block.id, float_title]) if block.id block.level = float_level block.title = float_title break # FIXME create another set for "passthrough" styles # FIXME make this more DRY! elsif !style.nil? && style != 'normal' if PARAGRAPH_STYLES.include?(style) block_context = style.to_sym cloaked_context = :paragraph reader.unshift_line this_line # advance to block parsing => break elsif ADMONITION_STYLES.include?(style) block_context = :admonition cloaked_context = :paragraph reader.unshift_line this_line # advance to block parsing => break elsif block_extensions && extensions.processor_registered_for_block?(style, :paragraph) block_context = style.to_sym cloaked_context = :paragraph reader.unshift_line this_line # advance to block parsing => break else warn "asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style}" style = nil # continue to process paragraph end end break_at_list = (skipped == 0 && in_list) # a literal paragraph is contiguous lines starting at least one space if style != 'normal' && this_line.match(REGEXP[:lit_par]) # So we need to actually include this one in the read_lines group reader.unshift_line this_line lines = reader.read_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true) {|line| # a preceding blank line (skipped > 0) indicates we are in a list continuation # and therefore we should not break at a list item # (this won't stop breaking on item of same level since we've already parsed them out) # QUESTION can we turn this block into a lambda or function call? (break_at_list && line.match(REGEXP[:any_list])) || (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))) } reset_block_indent! lines block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes) # a literal gets special meaning inside of a definition list # TODO this feels hacky, better way to distinguish from explicit literal block? block.set_option('listparagraph') if in_list # a paragraph is contiguous nonblank/noncontinuation lines else reader.unshift_line this_line lines = reader.read_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true, :skip_line_comments => true) {|line| # a preceding blank line (skipped > 0) indicates we are in a list continuation # and therefore we should not break at a list item # (this won't stop breaking on item of same level since we've already parsed them out) # QUESTION can we turn this block into a lambda or function call? (break_at_list && line.match(REGEXP[:any_list])) || (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line]))) } # NOTE we need this logic because we've asked the reader to skip # line comments, which may leave us w/ an empty buffer if those # were the only lines found if lines.empty? # call advance since the reader preserved the last line reader.advance return nil end catalog_inline_anchors(lines.join, document) first_line = lines.first if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline])) lines[0] = admonition_match.post_match.lstrip attributes['style'] = admonition_match[1] attributes['name'] = admonition_name = admonition_match[1].downcase attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] block = Block.new(parent, :admonition, :source => lines, :attributes => attributes) elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ') lines.map! {|line| if line.start_with?('> ') line[2..-1] elsif line.chomp == '>' line[1..-1] else line end } if lines.last.start_with?('-- ') attribution, citetitle = lines.pop[3..-1].split(', ', 2) lines.pop while lines.last.chomp.empty? lines[-1] = lines.last.chomp else attribution, citetitle = nil end attributes['style'] = 'quote' attributes['attribution'] = attribution unless attribution.nil? attributes['citetitle'] = citetitle unless citetitle.nil? # NOTE will only detect headings that are floating titles (not section titles) # TODO could assume a floating title when inside a block context # FIXME Reader needs to be created w/ line info block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes) elsif !text_only && lines.size > 1 && first_line.start_with?('"') && lines.last.start_with?('-- ') && lines[-2].chomp.end_with?('"') lines[0] = first_line[1..-1] attribution, citetitle = lines.pop[3..-1].split(', ', 2) lines.pop while lines.last.chomp.empty? lines[-1] = lines.last.chomp.chop attributes['style'] = 'quote' attributes['attribution'] = attribution unless attribution.nil? attributes['citetitle'] = citetitle unless citetitle.nil? block = Block.new(parent, :quote, :source => lines, :attributes => attributes) #block = Block.new(parent, :quote, :content_model => :compound, :attributes => attributes) #block << Block.new(block, :paragraph, :source => lines) else # if [normal] is used over an indented paragraph, unindent it if style == 'normal' && ((first_char = lines.first[0..0]) == ' ' || first_char == "\t") first_line = lines.first first_line_shifted = first_line.lstrip indent = line_length(first_line) - line_length(first_line_shifted) lines[0] = first_line_shifted # QUESTION should we fix the rest of the lines, since in XML output it's insignificant? lines.size.times do |i| lines[i] = lines[i][indent..-1] if i > 0 end end block = Block.new(parent, :paragraph, :source => lines, :attributes => attributes) end end # forbid loop from executing more than once break end end # either delimited block or styled paragraph if block.nil? && !block_context.nil? # abstract and partintro should be handled by open block # FIXME kind of hackish...need to sort out how to generalize this block_context = :open if block_context == :abstract || block_context == :partintro case block_context when :admonition attributes['name'] = admonition_name = style.downcase attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] block = build_block(block_context, :compound, terminator, parent, reader, attributes) when :comment build_block(block_context, :skip, terminator, parent, reader, attributes) return nil when :example block = build_block(block_context, :compound, terminator, parent, reader, attributes, {:supports_caption => true}) when :listing, :fenced_code, :source if block_context == :fenced_code style = attributes['style'] = 'source' language, linenums = this_line[3...-1].split(',', 2) if language && !(language = language.strip).empty? attributes['language'] = language attributes['linenums'] = '' if linenums && !linenums.strip.empty? end terminator = terminator[0..2] elsif block_context == :source AttributeList.rekey(attributes, [nil, 'language', 'linenums']) end block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true}) when :literal block = build_block(block_context, :verbatim, terminator, parent, reader, attributes) when :pass block = build_block(block_context, :raw, terminator, parent, reader, attributes) when :open, :sidebar block = build_block(block_context, :compound, terminator, parent, reader, attributes) when :table cursor = reader.cursor block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor case terminator[0..0] when ',' attributes['format'] = 'csv' when ':' attributes['format'] = 'dsv' end block = next_table(block_reader, parent, attributes) when :quote, :verse AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle']) block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes) else if block_extensions && extensions.processor_registered_for_block?(block_context, cloaked_context) processor = extensions.load_block_processor block_context, document if (content_model = processor.options[:content_model]) != :skip if !(pos_attrs = processor.options.fetch(:pos_attrs, [])).empty? AttributeList.rekey(attributes, [nil].concat(pos_attrs)) end if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty? default_attrs.each {|k, v| attributes[k] ||= v } end end block = build_block(block_context, content_model, terminator, parent, reader, attributes, :processor => processor) return nil if block.nil? else # this should only happen if there's a misconfiguration raise "Unsupported block type #{block_context} at #{reader.line_info}" end end end end # when looking for nested content, one or more line comments, comment # blocks or trailing attribute lists could leave us without a block, # so handle accordingly # REVIEW we may no longer need this nil check if !block.nil? # REVIEW seems like there is a better way to organize this wrap-up block.id ||= attributes['id'] if attributes.has_key?('id') block.title = attributes['title'] unless block.title? block.caption ||= attributes.delete('caption') # TODO eventualy remove the style attribute from the attributes hash #block.style = attributes.delete('style') block.style = attributes['style'] # AsciiDoc always use [id] as the reftext in HTML output, # but I'd like to do better in Asciidoctor if block.id && block.title? && !attributes.has_key?('reftext') document.register(:ids, [block.id, block.title]) end block.update_attributes(attributes) block.lock_in_subs #if document.attributes.has_key? :pending_attribute_entries # document.attributes.delete(:pending_attribute_entries).each do |entry| # entry.save_to block.attributes # end #end if block.sub? :callouts if !(catalog_callouts block.source, document) # No need to look for callouts if they aren't there block.remove_sub :callouts end end end block end # Public: Determines whether this line is the start of any of the delimited blocks # # returns the match data if this line is the first line of a delimited block or nil if not def self.is_delimited_block? line, return_match_data = false # highly optimized for best performance line_len = line.length - 1 return nil unless line_len > 1 && DELIMITED_BLOCK_LEADERS.include?(line[0..1]) line = line.chomp # counts endline character in line length if line_len == 2 tip = line tl = 2 elsif line_len < 3 return nil else if line_len < 5 tip = line tl = line_len else tip = line[0..3] tl = 4 end # special case for fenced code blocks if Compliance.markdown_syntax tip_alt = tip.chop if tl == 4 if tip_alt == '```' if tip.end_with? '`' return nil end tip = tip_alt tl = 3 elsif tip_alt == '~~~' if tip.end_with? '~' return nil end tip = tip_alt tl = 3 end end end if DELIMITED_BLOCKS.has_key? tip # tip is the full line when delimiter is minimum length if tl == 3 || tl == line_len if return_match_data context, masq = *DELIMITED_BLOCKS[tip] BlockMatchData.new(context, masq, tip, tip) else true end elsif %(#{tip}#{tip[-1..-1] * (line_len - tl)}) == line if return_match_data context, masq = *DELIMITED_BLOCKS[tip] BlockMatchData.new(context, masq, tip, line) else true end #elsif match = line.match(REGEXP[:any_blk]) # if return_match_data # context, masq = *DELIMITED_BLOCKS[tip] # BlockMatchData.new(context, masq, tip, match[0]) # else # true # end else nil end else nil end end # whether a block supports complex content should be a config setting # if terminator is false, that means the all the lines in the reader should be parsed # NOTE could invoke filter in here, before and after parsing def self.build_block(block_context, content_model, terminator, parent, reader, attributes, options = {}) if content_model == :skip || content_model == :raw skip_processing = content_model == :skip parse_as_content_model = :simple else skip_processing = false parse_as_content_model = content_model end if terminator.nil? if parse_as_content_model == :verbatim lines = reader.read_lines_until(:break_on_blank_lines => true, :break_on_list_continuation => true) else content_model = :simple if content_model == :compound lines = reader.read_lines_until( :break_on_blank_lines => true, :break_on_list_continuation => true, :preserve_last_line => true, :skip_line_comments => true, :skip_processing => skip_processing) {|line| COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])) } # QUESTION check for empty lines after grabbing lines for simple content model? end block_reader = nil elsif parse_as_content_model != :compound lines = reader.read_lines_until(:terminator => terminator, :chomp_last_line => true, :skip_processing => skip_processing) block_reader = nil # terminator is false when reader has already been prepared elsif terminator == false lines = nil block_reader = reader else lines = nil cursor = reader.cursor block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_processing => skip_processing), cursor end if content_model == :skip attributes.clear return lines end if content_model == :verbatim && attributes.has_key?('indent') reset_block_indent! lines, attributes['indent'].to_i end if (processor = options[:processor]) attributes.delete('style') processor.options[:content_model] = content_model block = processor.process(parent, block_reader || Reader.new(lines), attributes) else block = Block.new(parent, block_context, :content_model => content_model, :attributes => attributes, :source => lines) end # should supports_caption be necessary? if options.fetch(:supports_caption, false) block.title = attributes.delete('title') if attributes.has_key?('title') block.assign_caption attributes.delete('caption') end if content_model == :compound # we can look for blocks until there are no more lines (and not worry # about sections) since the reader is confined within the boundaries of a # delimited block parse_blocks block_reader, block end block end # Public: Parse blocks from this reader until there are no more lines. # # This method calls Lexer#next_block until there are no more lines in the # Reader. It does not consider sections because it's assumed the Reader only # has lines which are within a delimited block region. # # reader - The Reader containing the lines to process # parent - The parent Block to which to attach the parsed blocks # # Returns nothing. def self.parse_blocks(reader, parent) while reader.has_more_lines? block = Lexer.next_block(reader, parent) parent << block unless block.nil? end end # Internal: Parse and construct an outline list Block from the current position of the Reader # # reader - The Reader from which to retrieve the outline list # list_type - A Symbol representing the list type (:olist for ordered, :ulist for unordered) # parent - The parent Block to which this outline list belongs # # Returns the Block encapsulating the parsed outline (unordered or ordered) list def self.next_outline_list(reader, list_type, parent) list_block = List.new(parent, list_type) if parent.context == list_type list_block.level = parent.level + 1 else list_block.level = 1 end #Debug.debug { "Created #{list_type} block: #{list_block}" } while reader.has_more_lines? && (match = reader.peek_line.match(REGEXP[list_type])) marker = resolve_list_marker(list_type, match[1]) # if we are moving to the next item, and the marker is different # determine if we are moving up or down in nesting if list_block.items? && marker != list_block.items.first.marker # assume list is nested by default, but then check to see if we are # popping out of a nested list by matching an ancestor's list marker this_item_level = list_block.level + 1 ancestor = parent while ancestor.context == list_type if marker == ancestor.items.first.marker this_item_level = ancestor.level break end ancestor = ancestor.parent end else this_item_level = list_block.level end if !list_block.items? || this_item_level == list_block.level list_item = next_list_item(reader, list_block, match) elsif this_item_level < list_block.level # leave this block break elsif this_item_level > list_block.level # If this next list level is down one from the # current Block's, append it to content of the current list item list_block.items.last << next_block(reader, list_block) end list_block << list_item unless list_item.nil? list_item = nil reader.skip_blank_lines end list_block end # Internal: Catalog any callouts found in the text, but don't process them # # text - The String of text in which to look for callouts # document - The current document on which the callouts are stored # # Returns A Boolean indicating whether callouts were found def self.catalog_callouts(text, document) found = false if text.include? '<' text.scan(REGEXP[:callout_quick_scan]) { # alias match for Ruby 1.8.7 compat m = $~ if m[0][0..0] != '\\' document.callouts.register(m[2]) end # we have to mark as found even if it's escaped so it can be unescaped found = true } end found end # Internal: Catalog any inline anchors found in the text, but don't process them # # text - The String text in which to look for inline anchors # document - The current document on which the references are stored # # Returns nothing def self.catalog_inline_anchors(text, document) text.scan(REGEXP[:anchor_macro]) { # alias match for Ruby 1.8.7 compat m = $~ next if m[0].start_with? '\\' id, reftext = m[1].split(',') id.sub!(REGEXP[:dbl_quoted], '\2') if !reftext.nil? reftext.sub!(REGEXP[:m_dbl_quoted], '\2') end document.register(:ids, [id, reftext]) } nil end # Internal: Parse and construct a labeled (e.g., definition) list Block from the current position of the Reader # # reader - The Reader from which to retrieve the labeled list # match - The Regexp match for the head of the list # parent - The parent Block to which this labeled list belongs # # Returns the Block encapsulating the parsed labeled list def self.next_labeled_list(reader, match, parent) list_block = List.new(parent, :dlist) previous_pair = nil # allows us to capture until we find a labeled item # that uses the same delimiter (::, :::, :::: or ;;) sibling_pattern = REGEXP[:dlist_siblings][match[2]] begin term, item = next_list_item(reader, list_block, match, sibling_pattern) if !previous_pair.nil? && previous_pair.last.nil? previous_pair.pop previous_pair[0] << term previous_pair << item else # FIXME this misses the automatic parent assignment list_block.items << (previous_pair = [[term], item]) end end while reader.has_more_lines? && match = reader.peek_line.match(sibling_pattern) list_block end # Internal: Parse and construct the next ListItem for the current bulleted # (unordered or ordered) list Block, callout lists included, or the next # term ListItem and definition ListItem pair for the labeled list Block. # # First collect and process all the lines that constitute the next list # item for the parent list (according to its type). Next, parse those lines # into blocks and associate them with the ListItem (in the case of a # labeled list, the definition ListItem). Finally, fold the first block # into the item's text attribute according to rules described in ListItem. # # reader - The Reader from which to retrieve the next list item # list_block - The parent list Block of this ListItem. Also provides access to the list type. # match - The match Array which contains the marker and text (first-line) of the ListItem # sibling_trait - The list marker or the Regexp to match a sibling item # # Returns the next ListItem or ListItem pair (depending on the list type) # for the parent list Block. def self.next_list_item(reader, list_block, match, sibling_trait = nil) list_type = list_block.context if list_type == :dlist list_term = ListItem.new(list_block, match[1]) list_item = ListItem.new(list_block, match[3]) has_text = !match[3].to_s.empty? else # Create list item using first line as the text of the list item text = match[2] checkbox = false if list_type == :ulist && text.start_with?('[') if text.start_with? '[ ] ' checkbox = true checked = false text = text[3..-1].lstrip elsif text.start_with?('[*] ') || text.start_with?('[x] ') checkbox = true checked = true text = text[3..-1].lstrip end end list_item = ListItem.new(list_block, text) if checkbox # FIXME checklist never makes it into the options attribute list_block.attributes['checklist-option'] = '' list_item.attributes['checkbox'] = '' list_item.attributes['checked'] = '' if checked end if !sibling_trait sibling_trait = resolve_list_marker(list_type, match[1], list_block.items.size, true, reader) end list_item.marker = sibling_trait has_text = true end # first skip the line with the marker / term reader.advance cursor = reader.cursor list_item_reader = Reader.new read_lines_for_list_item(reader, list_type, sibling_trait, has_text), cursor if list_item_reader.has_more_lines? comment_lines = list_item_reader.skip_line_comments subsequent_line = list_item_reader.peek_line list_item_reader.unshift_lines comment_lines unless comment_lines.empty? if !subsequent_line.nil? continuation_connects_first_block = (subsequent_line == ::Asciidoctor::EOL) # if there's no continuation connecting the first block, then # treat the lines as paragraph text (activated when has_text = false) if !continuation_connects_first_block && list_type != :dlist has_text = false end content_adjacent = !subsequent_line.chomp.empty? else continuation_connects_first_block = false content_adjacent = false end # only relevant for :dlist options = {:text => !has_text} # we can look for blocks until there are no more lines (and not worry # about sections) since the reader is confined within the boundaries of a # list while list_item_reader.has_more_lines? new_block = next_block(list_item_reader, list_block, {}, options) list_item << new_block unless new_block.nil? end list_item.fold_first(continuation_connects_first_block, content_adjacent) end if list_type == :dlist unless list_item.text? || list_item.blocks? list_item = nil end [list_term, list_item] else list_item end end # Internal: Collect the lines belonging to the current list item, navigating # through all the rules that determine what comprises a list item. # # Grab lines until a sibling list item is found, or the block is broken by a # terminator (such as a line comment). Definition lists are more greedy if # they don't have optional inline item text...they want that text # # reader - The Reader from which to retrieve the lines. # list_type - The Symbol context of the list (:ulist, :olist, :colist or :dlist) # sibling_trait - A Regexp that matches a sibling of this list item or String list marker # of the items in this list (default: nil) # has_text - Whether the list item has text defined inline (always true except for labeled lists) # # Returns an Array of lines belonging to the current list item. def self.read_lines_for_list_item(reader, list_type, sibling_trait = nil, has_text = true) buffer = [] # three states for continuation: :inactive, :active & :frozen # :frozen signifies we've detected sequential continuation lines & # continuation is not permitted until reset continuation = :inactive # if we are within a nested list, we don't throw away the list # continuation marks because they will be processed when grabbing # the lines for those nested lists within_nested_list = false # a detached continuation is a list continuation that follows a blank line # it gets associated with the outermost block detached_continuation = nil while reader.has_more_lines? this_line = reader.read_line # if we've arrived at a sibling item in this list, we've captured # the complete list item and can begin processing it # the remainder of the method determines whether we've reached # the termination of the list break if is_sibling_list_item?(this_line, list_type, sibling_trait) prev_line = buffer.empty? ? nil : buffer.last.chomp if prev_line == LIST_CONTINUATION if continuation == :inactive continuation = :active has_text = true buffer[-1] = ::Asciidoctor::EOL unless within_nested_list end # dealing with adjacent list continuations (which is really a syntax error) if this_line.chomp == LIST_CONTINUATION if continuation != :frozen continuation = :frozen buffer << this_line end this_line = nil next end end # a delimited block immediately breaks the list unless preceded # by a list continuation (they are harsh like that ;0) if match = is_delimited_block?(this_line, true) if continuation == :active buffer << this_line # grab all the lines in the block, leaving the delimiters in place # we're being more strict here about the terminator, but I think that's a good thing buffer.concat reader.read_lines_until(:terminator => match.terminator, :read_last_line => true) continuation = :inactive else break end # technically attr_line only breaks if ensuing line is not a list item # which really means attr_line only breaks if it's acting as a block delimiter elsif list_type == :dlist && continuation != :active && this_line.match(REGEXP[:attr_line]) break else if continuation == :active && !this_line.chomp.empty? # literal paragraphs have special considerations (and this is one of # two entry points into one) # if we don't process it as a whole, then a line in it that looks like a # list item will throw off the exit from it if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line buffer.concat reader.read_lines_until( :preserve_last_line => true, :break_on_blank_lines => true, :break_on_list_continuation => true) {|line| # we may be in an indented list disguised as a literal paragraph # so we need to make sure we don't slurp up a legitimate sibling list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) } continuation = :inactive # let block metadata play out until we find the block elsif this_line.match(REGEXP[:blk_title]) || this_line.match(REGEXP[:attr_line]) || this_line.match(REGEXP[:attr_entry]) buffer << this_line else if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| this_line.match(REGEXP[ctx]) } within_nested_list = true if nested_list_type == :dlist && $~[3].to_s.empty? # get greedy again has_text = false end end buffer << this_line continuation = :inactive end elsif !prev_line.nil? && prev_line.chomp.empty? # advance to the next line of content if this_line.chomp.empty? reader.skip_blank_lines this_line = reader.read_line # if we hit eof or a sibling, stop reading break if this_line.nil? || is_sibling_list_item?(this_line, list_type, sibling_trait) end if this_line.chomp == LIST_CONTINUATION detached_continuation = buffer.size buffer << this_line else # has_text is only relevant for dlist, which is more greedy until it has text for an item # for all other lists, has_text is always true # in this block, we have to see whether we stay in the list if has_text # slurp up any literal paragraph offset by blank lines if this_line.match(REGEXP[:lit_par]) reader.unshift_line this_line buffer.concat reader.read_lines_until( :preserve_last_line => true, :break_on_blank_lines => true, :break_on_list_continuation => true) {|line| # we may be in an indented list disguised as a literal paragraph # so we need to make sure we don't slurp up a legitimate sibling list_type == :dlist && is_sibling_list_item?(line, list_type, sibling_trait) } # TODO any way to combine this with the check after skipping blank lines? elsif is_sibling_list_item?(this_line, list_type, sibling_trait) break elsif nested_list_type = NESTABLE_LIST_CONTEXTS.detect {|ctx| this_line.match(REGEXP[ctx]) } buffer << this_line within_nested_list = true if nested_list_type == :dlist && $~[3].to_s.empty? # get greedy again has_text = false end else break end else # only dlist in need of item text, so slurp it up! # pop the blank line so it's not interpretted as a list continuation buffer.pop unless within_nested_list buffer << this_line has_text = true end end else has_text = true if !this_line.chomp.empty? if nested_list_type = (within_nested_list ? [:dlist] : NESTABLE_LIST_CONTEXTS).detect {|ctx| this_line.match(REGEXP[ctx]) } within_nested_list = true if nested_list_type == :dlist && $~[3].to_s.empty? # get greedy again has_text = false end end buffer << this_line end end this_line = nil end reader.unshift_line this_line if !this_line.nil? if detached_continuation buffer.delete_at detached_continuation end # strip trailing blank lines to prevent empty blocks buffer.pop while !buffer.empty? && buffer.last.chomp.empty? # We do need to replace the optional trailing continuation # a blank line would have served the same purpose in the document if !buffer.empty? && buffer.last.chomp == LIST_CONTINUATION buffer.pop end #puts "BUFFER[#{list_type},#{sibling_trait}]>#{buffer.join}#{buffer.inspect} ["Foo\n", "~~~\n"] # # title, level, id, single = parse_section_title(reader, document) # # title # # => "Foo" # level # # => 2 # id # # => nil # single # # => false # # line1 # # => "==== Foo\n" # # title, level, id, single = parse_section_title(reader, document) # # title # # => "Foo" # level # # => 3 # id # # => nil # single # # => true # # returns an Array of [String, Integer, String, Boolean], representing the # id, title, level and line count of the Section, or nil. # #-- # NOTE for efficiency, we don't reuse methods that check for a section title def self.parse_section_title(reader, document) line1 = reader.read_line sect_id = nil sect_title = nil sect_level = -1 single_line = true first_char = line1[0..0] if (first_char == '=' || (Compliance.markdown_syntax && first_char == '#')) && (match = line1.match(REGEXP[:section_title])) sect_id = match[3] sect_title = match[2] sect_level = single_line_section_level match[1] elsif Compliance.underline_style_section_titles line2 = reader.peek_line true if !line2.nil? && SECTION_LEVELS.has_key?(line2[0..0]) && line2.match(REGEXP[:section_underline]) && (name_match = line1.match(REGEXP[:section_name])) && # chomp so that a (non-visible) endline does not impact calculation (line_length(line1) - line_length(line2)).abs <= 1 if anchor_match = name_match[1].match(REGEXP[:anchor_embedded]) sect_id = anchor_match[2] sect_title = anchor_match[1] else sect_title = name_match[1] end sect_level = section_level line2 single_line = false reader.advance end end if sect_level >= 0 sect_level += document.attr('leveloffset', 0).to_i end [sect_id, sect_title, sect_level, single_line] end # Public: Calculate the number of unicode characters in the line, excluding the endline # # line - the String to calculate # # returns the number of unicode characters in the line def self.line_length(line) FORCE_UNICODE_LINE_LENGTH ? line.chomp.scan(/./u).length : line.chomp.length end # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info). # # Returns the Hash of header metadata. If a Document object is supplied, the metadata # is applied directly to the attributes of the Document. # # reader - the Reader holding the source lines of the document # document - the Document we are building (default: nil) # # Examples # # parse_header_metadata(Reader.new ["Author Name \n", "v1.0, 2012-12-21: Coincide w/ end of world.\n"]) # # => {'author' => 'Author Name', 'firstname' => 'Author', 'lastname' => 'Name', 'email' => 'author@example.org', # # 'revnumber' => '1.0', 'revdate' => '2012-12-21', 'revremark' => 'Coincide w/ end of world.'} def self.parse_header_metadata(reader, document = nil) # NOTE this will discard away any comment lines, but not skip blank lines process_attribute_entries(reader, document) metadata = {} implicit_author = nil implicit_authors = nil if reader.has_more_lines? && !reader.next_line_empty? author_metadata = process_authors reader.read_line unless author_metadata.empty? # apply header subs and assign to document if !document.nil? author_metadata.map do |key, val| val = val.is_a?(String) ? document.apply_header_subs(val) : val document.attributes[key] = val if !document.attributes.has_key?(key) val end implicit_author = document.attributes['author'] implicit_authors = document.attributes['authors'] end metadata = author_metadata end # NOTE this will discard any comment lines, but not skip blank lines process_attribute_entries(reader, document) rev_metadata = {} if reader.has_more_lines? && !reader.next_line_empty? rev_line = reader.read_line if match = rev_line.match(REGEXP[:revision_info]) rev_metadata['revdate'] = match[2].strip rev_metadata['revnumber'] = match[1].rstrip unless match[1].nil? rev_metadata['revremark'] = match[3].rstrip unless match[3].nil? else # throw it back reader.unshift_line rev_line end end unless rev_metadata.empty? # apply header subs and assign to document if !document.nil? rev_metadata.map do |key, val| val = document.apply_header_subs(val) document.attributes[key] = val if !document.attributes.has_key?(key) val end end metadata.update rev_metadata end # NOTE this will discard any comment lines, but not skip blank lines process_attribute_entries(reader, document) reader.skip_blank_lines end if !document.nil? # process author attribute entries that override (or stand in for) the implicit author line author_metadata = nil if document.attributes.has_key?('author') && (author_line = document.attributes['author']) != implicit_author # do not allow multiple, process as names only author_metadata = process_authors author_line, true, false elsif document.attributes.has_key?('authors') && (author_line = document.attributes['authors']) != implicit_authors # allow multiple, process as names only author_metadata = process_authors author_line, true else authors = [] author_key = "author_#{authors.size + 1}" while document.attributes.has_key? author_key authors << document.attributes[author_key] author_key = "author_#{authors.size + 1}" end if authors.size == 1 # do not allow multiple, process as names only author_metadata = process_authors authors.first, true, false elsif authors.size > 1 # allow multiple, process as names only author_metadata = process_authors authors.join('; '), true end end unless author_metadata.nil? document.attributes.update author_metadata # special case if !document.attributes.has_key?('email') && document.attributes.has_key?('email_1') document.attributes['email'] = document.attributes['email_1'] end end end metadata end # Internal: Parse the author line into a Hash of author metadata # # author_line - the String author line # names_only - a Boolean flag that indicates whether to process line as # names only or names with emails (default: false) # multiple - a Boolean flag that indicates whether to process multiple # semicolon-separated entries in the author line (default: true) # # returns a Hash of author metadata def self.process_authors(author_line, names_only = false, multiple = true) author_metadata = {} keys = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email'] author_entries = multiple ? author_line.split(';').map(&:strip) : [author_line] author_entries.each_with_index do |author_entry, idx| author_entry.strip! next if author_entry.empty? key_map = {} if idx.zero? keys.each do |key| key_map[key.to_sym] = key end else keys.each do |key| key_map[key.to_sym] = "#{key}_#{idx + 1}" end end segments = nil if names_only # splitting on ' ' will collapse repeating spaces segments = author_entry.split(' ', 3) elsif (match = author_entry.match(REGEXP[:author_info])) segments = match.to_a segments.shift end unless segments.nil? author_metadata[key_map[:firstname]] = fname = segments[0].tr('_', ' ') author_metadata[key_map[:author]] = fname author_metadata[key_map[:authorinitials]] = fname[0, 1] if !segments[1].nil? && !segments[2].nil? author_metadata[key_map[:middlename]] = mname = segments[1].tr('_', ' ') author_metadata[key_map[:lastname]] = lname = segments[2].tr('_', ' ') author_metadata[key_map[:author]] = [fname, mname, lname].join ' ' author_metadata[key_map[:authorinitials]] = [fname[0, 1], mname[0, 1], lname[0, 1]].join elsif !segments[1].nil? author_metadata[key_map[:lastname]] = lname = segments[1].tr('_', ' ') author_metadata[key_map[:author]] = [fname, lname].join ' ' author_metadata[key_map[:authorinitials]] = [fname[0, 1], lname[0, 1]].join end author_metadata[key_map[:email]] = segments[3] unless names_only || segments[3].nil? else author_metadata[key_map[:author]] = author_metadata[key_map[:firstname]] = fname = author_entry.strip.squeeze(' ') author_metadata[key_map[:authorinitials]] = fname[0, 1] end author_metadata['authorcount'] = idx + 1 # only assign the _1 attributes if there are multiple authors if idx == 1 keys.each do |key| author_metadata["#{key}_1"] = author_metadata[key] if author_metadata.has_key? key end end if idx.zero? author_metadata['authors'] = author_metadata[key_map[:author]] else author_metadata['authors'] = "#{author_metadata['authors']}, #{author_metadata[key_map[:author]]}" end end author_metadata end # Internal: Parse lines of metadata until a line of metadata is not found. # # This method processes sequential lines containing block metadata, ignoring # blank lines and comments. # # reader - the source reader # parent - the parent to which the lines belong # attributes - a Hash of attributes in which any metadata found will be stored (default: {}) # options - a Hash of options to control processing: (default: {}) # * :text indicates that lexer is only looking for text content # and thus the block title should not be captured # # returns the Hash of attributes including any metadata found def self.parse_block_metadata_lines(reader, parent, attributes = {}, options = {}) while parse_block_metadata_line(reader, parent, attributes, options) # discard the line just processed reader.advance reader.skip_blank_lines end attributes end # Internal: Parse the next line if it contains metadata for the following block # # This method handles lines with the following content: # # * line or block comment # * anchor # * attribute list # * block title # # Any attributes found will be inserted into the attributes argument. # If the line contains block metadata, the method returns true, otherwise false. # # reader - the source reader # parent - the parent of the current line # attributes - a Hash of attributes in which any metadata found will be stored # options - a Hash of options to control processing: (default: {}) # * :text indicates that lexer is only looking for text content # and thus the block title should not be captured # # returns true if the line contains metadata, otherwise false def self.parse_block_metadata_line(reader, parent, attributes, options = {}) return false if !reader.has_more_lines? next_line = reader.peek_line if (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk])) terminator = match[0] reader.read_lines_until(:skip_first_line => true, :preserve_last_line => true, :terminator => terminator, :skip_processing => true) elsif commentish && next_line.match(REGEXP[:comment]) # do nothing, we'll skip it elsif !options[:text] && (match = next_line.match(REGEXP[:attr_entry])) process_attribute_entry(reader, parent, attributes, match) elsif match = next_line.match(REGEXP[:anchor]) id, reftext = match[1].split(',') attributes['id'] = id # AsciiDoc always uses [id] as the reftext in HTML output, # but I'd like to do better in Asciidoctor #parent.document.register(:ids, id) if reftext attributes['reftext'] = reftext parent.document.register(:ids, [id, reftext]) end elsif match = next_line.match(REGEXP[:blk_attr_list]) parent.document.parse_attributes(match[1], [], :sub_input => true, :into => attributes) # NOTE title doesn't apply to section, but we need to stash it for the first block # TODO should issue an error if this is found above the document title elsif !options[:text] && (match = next_line.match(REGEXP[:blk_title])) attributes['title'] = match[1] else return false end true end def self.process_attribute_entries(reader, parent, attributes = nil) reader.skip_comment_lines while process_attribute_entry(reader, parent, attributes) # discard line just processed reader.advance reader.skip_comment_lines end end def self.process_attribute_entry(reader, parent, attributes = nil, match = nil) match ||= reader.has_more_lines? ? reader.peek_line.match(REGEXP[:attr_entry]) : nil if match name = match[1] value = match[2].nil? ? '' : match[2] if value.end_with? LINE_BREAK value.chop!.rstrip! while reader.advance next_line = reader.peek_line.strip break if next_line.empty? if next_line.end_with? LINE_BREAK value = "#{value} #{next_line.chop.rstrip}" else value = "#{value} #{next_line}" break end end end store_attribute(name, value, parent.nil? ? nil : parent.document, attributes) true else false end end # Public: Store the attribute in the document and register attribute entry if accessible # # name - the String name of the attribute to store # value - the String value of the attribute to store # doc - the Document being parsed # attrs - the attributes for the current context # # returns a 2-element array containing the attribute name and value def self.store_attribute(name, value, doc = nil, attrs = nil) if name.end_with?('!') # a nil value signals the attribute should be deleted (undefined) value = nil name = name.chop elsif name.start_with?('!') # a nil value signals the attribute should be deleted (undefined) value = nil name = name[1..-1] end name = sanitize_attribute_name(name) accessible = true unless doc.nil? accessible = value.nil? ? doc.delete_attribute(name) : doc.set_attribute(name, value) end unless !accessible || attrs.nil? Document::AttributeEntry.new(name, value).save_to(attrs) end [name, value] end # Internal: Resolve the 0-index marker for this list item # # For ordered lists, match the marker used for this list item against the # known list markers and determine which marker is the first (0-index) marker # in its number series. # # For callout lists, return <1>. # # For bulleted lists, return the marker as passed to this method. # # list_type - The Symbol context of the list # marker - The String marker for this list item # ordinal - The position of this list item in the list # validate - Whether to validate the value of the marker # # Returns the String 0-index marker for this list item def self.resolve_list_marker(list_type, marker, ordinal = 0, validate = false, reader = nil) if list_type == :olist && !marker.start_with?('.') resolve_ordered_list_marker(marker, ordinal, validate, reader) elsif list_type == :colist '<1>' else marker end end # Internal: Resolve the 0-index marker for this ordered list item # # Match the marker used for this ordered list item against the # known ordered list markers and determine which marker is # the first (0-index) marker in its number series. # # The purpose of this method is to normalize the implicit numbered markers # so that they can be compared against other list items. # # marker - The marker used for this list item # ordinal - The 0-based index of the list item (default: 0) # validate - Perform validation that the marker provided is the proper # marker in the sequence (default: false) # # Examples # # marker = 'B.' # Lexer::resolve_ordered_list_marker(marker, 1, true) # # => 'A.' # # Returns the String of the first marker in this number series def self.resolve_ordered_list_marker(marker, ordinal = 0, validate = false, reader = nil) number_style = ORDERED_LIST_STYLES.detect {|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) } expected = actual = nil case number_style when :arabic if validate expected = ordinal + 1 actual = marker.to_i end marker = '1.' when :loweralpha if validate expected = ('a'[0].ord + ordinal).chr actual = marker.chomp('.') end marker = 'a.' when :upperalpha if validate expected = ('A'[0].ord + ordinal).chr actual = marker.chomp('.') end marker = 'A.' when :lowerroman if validate # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb expected = ordinal + 1 actual = roman_numeral_to_int(marker.chomp(')')) end marker = 'i)' when :upperroman if validate # TODO report this in roman numerals; see https://github.com/jamesshipton/roman-numeral/blob/master/lib/roman_numeral.rb expected = ordinal + 1 actual = roman_numeral_to_int(marker.chomp(')')) end marker = 'I)' end if validate && expected != actual warn "asciidoctor: WARNING: #{reader.line_info}: list item index: expected #{expected}, got #{actual}" end marker end # Internal: Determine whether the this line is a sibling list item # according to the list type and trait (marker) provided. # # line - The String line to check # list_type - The context of the list (:olist, :ulist, :colist, :dlist) # sibling_trait - The String marker for the list or the Regexp to match a sibling # # Returns a Boolean indicating whether this line is a sibling list item given # the criteria provided def self.is_sibling_list_item?(line, list_type, sibling_trait) if sibling_trait.is_a?(Regexp) matcher = sibling_trait expected_marker = false else matcher = REGEXP[list_type] expected_marker = sibling_trait end if m = line.match(matcher) if expected_marker expected_marker == resolve_list_marker(list_type, m[1]) else true end else false end end # Internal: Parse the table contained in the provided Reader # # table_reader - a Reader containing the source lines of an AsciiDoc table # parent - the parent Block of this Asciidoctor::Table # attributes - attributes captured from above this Block # # returns an instance of Asciidoctor::Table parsed from the provided reader def self.next_table(table_reader, parent, attributes) table = Table.new(parent, attributes) table.title = attributes.delete('title') if attributes.has_key?('title') table.assign_caption attributes.delete('caption') if attributes.has_key? 'cols' table.create_columns(parse_col_specs(attributes['cols'])) explicit_col_specs = true else explicit_col_specs = false end skipped = table_reader.skip_blank_lines parser_ctx = Table::ParserContext.new(table_reader, table, attributes) loop_idx = -1 while table_reader.has_more_lines? loop_idx += 1 line = table_reader.read_line if skipped == 0 && loop_idx.zero? && !attributes.has_key?('options') && !(next_line = table_reader.peek_line).nil? && next_line == ::Asciidoctor::EOL table.has_header_option = true table.set_option 'header' end if parser_ctx.format == 'psv' if parser_ctx.starts_with_delimiter? line line = line[1..-1] # push an empty cell spec if boundary at start of line parser_ctx.close_open_cell else next_cell_spec, line = parse_cell_spec(line, :start) # if the cell spec is not null, then we're at a cell boundary if !next_cell_spec.nil? parser_ctx.close_open_cell next_cell_spec else # QUESTION do we not advance to next line? if so, when will we if we came into this block? end end end while !line.empty? if m = parser_ctx.match_delimiter(line) if parser_ctx.format == 'csv' if parser_ctx.buffer_has_unclosed_quotes?(m.pre_match) # throw it back, it's too small line = parser_ctx.skip_matched_delimiter(m) next end else if m.pre_match.end_with? '\\' line = parser_ctx.skip_matched_delimiter(m, true) next end end if parser_ctx.format == 'psv' next_cell_spec, cell_text = parse_cell_spec(m.pre_match, :end) parser_ctx.push_cell_spec next_cell_spec parser_ctx.buffer = %(#{parser_ctx.buffer}#{cell_text}) else parser_ctx.buffer = %(#{parser_ctx.buffer}#{m.pre_match}) end line = m.post_match parser_ctx.close_cell else # no other delimiters to see here # suck up this line into the buffer and move on parser_ctx.buffer = %(#{parser_ctx.buffer}#{line}) # QUESTION make stripping endlines in csv data an option? (unwrap-option?) if parser_ctx.format == 'csv' parser_ctx.buffer = %(#{parser_ctx.buffer.rstrip} ) end line = '' if parser_ctx.format == 'psv' || (parser_ctx.format == 'csv' && parser_ctx.buffer_has_unclosed_quotes?) parser_ctx.keep_cell_open else parser_ctx.close_cell true end end end skipped = table_reader.skip_blank_lines unless parser_ctx.cell_open? if !table_reader.has_more_lines? parser_ctx.close_cell true end end table.attributes['colcount'] ||= parser_ctx.col_count if !explicit_col_specs # TODO further encapsulate this logic (into table perhaps?) even_width = (100.0 / parser_ctx.col_count).floor table.columns.each {|c| c.assign_width(0, even_width) } end table.partition_header_footer attributes table end # Internal: Parse the column specs for this table. # # The column specs dictate the number of columns, relative # width of columns, default alignments for cells in each # column, and/or default styles or filters applied to the cells in # the column. # # Every column spec is guaranteed to have a width # # returns a Hash of attributes that specify how to format # and layout the cells in the table. def self.parse_col_specs(records) specs = [] # check for deprecated syntax if m = records.match(REGEXP[:digits]) 1.upto(m[0].to_i) { specs << {'width' => 1} } return specs end records.split(',').each {|record| # TODO might want to use scan rather than this mega-regexp if m = record.match(REGEXP[:table_colspec]) spec = {} if m[2] # make this an operation colspec, rowspec = m[2].split '.' if !colspec.to_s.empty? && Table::ALIGNMENTS[:h].has_key?(colspec) spec['halign'] = Table::ALIGNMENTS[:h][colspec] end if !rowspec.to_s.empty? && Table::ALIGNMENTS[:v].has_key?(rowspec) spec['valign'] = Table::ALIGNMENTS[:v][rowspec] end end # to_i permits us to support percentage width by stripping the % # NOTE this is slightly out of compliance w/ AsciiDoc, but makes way more sense spec['width'] = !m[3].nil? ? m[3].to_i : 1 # make this an operation if m[4] && Table::TEXT_STYLES.has_key?(m[4]) spec['style'] = Table::TEXT_STYLES[m[4]] end repeat = !m[1].nil? ? m[1].to_i : 1 1.upto(repeat) { specs << spec.dup } end } specs end # Internal: Parse the cell specs for the current cell. # # The cell specs dictate the cell's alignments, styles or filters, # colspan, rowspan and/or repeating content. # # returns the Hash of attributes that indicate how to layout # and style this cell in the table. def self.parse_cell_spec(line, pos = :start) # the default for the end pos it {} since we # know we're at a delimiter; when the pos # is start, we *may* be at a delimiter and # nil indicates we're not spec = (pos == :end ? {} : nil) rest = line if m = line.match(REGEXP[:table_cellspec][pos]) spec = {} return [spec, line] if m[0].chomp.empty? rest = (pos == :start ? m.post_match : m.pre_match) if m[1] colspec, rowspec = m[1].split '.' colspec = colspec.to_s.empty? ? 1 : colspec.to_i rowspec = rowspec.to_s.empty? ? 1 : rowspec.to_i if m[2] == '+' spec['colspan'] = colspec unless colspec == 1 spec['rowspan'] = rowspec unless rowspec == 1 elsif m[2] == '*' spec['repeatcol'] = colspec unless colspec == 1 end end if m[3] colspec, rowspec = m[3].split '.' if !colspec.to_s.empty? && Table::ALIGNMENTS[:h].has_key?(colspec) spec['halign'] = Table::ALIGNMENTS[:h][colspec] end if !rowspec.to_s.empty? && Table::ALIGNMENTS[:v].has_key?(rowspec) spec['valign'] = Table::ALIGNMENTS[:v][rowspec] end end if m[4] && Table::TEXT_STYLES.has_key?(m[4]) spec['style'] = Table::TEXT_STYLES[m[4]] end end [spec, rest] end # Public: Parse the first positional attribute and assign named attributes # # Parse the first positional attribute to extract the style, role and id # parts, assign the values to their cooresponding attribute keys and return # both the original style attribute and the parsed value from the first # positional attribute. # # attributes - The Hash of attributes to process and update # # Examples # # puts attributes # => {1 => "abstract#intro.lead%fragment", "style" => "preamble"} # # parse_style_attribute(attributes) # => ["abstract", "preamble"] # # puts attributes # => {1 => "abstract#intro.lead", "style" => "abstract", "id" => "intro", # "role" => "lead", "options" => ["fragment"], "fragment-option" => ''} # # Returns a two-element Array of the parsed style from the # first positional attribute and the original style that was # replaced def self.parse_style_attribute(attributes, reader = nil) original_style = attributes['style'] raw_style = attributes[1] # NOTE spaces are not allowed in shorthand, so if we find one, this ain't shorthand if !raw_style || raw_style.include?(' ') attributes['style'] = raw_style [raw_style, original_style] else type = :style collector = [] parsed = {} # QUESTION should this be a private method? (though, it's never called if shorthand isn't used) save_current = lambda { if collector.empty? if type != :style warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} invalid empty #{type} detected in style attribute" end else case type when :role, :option parsed[type] ||= [] parsed[type].push collector.join when :id if parsed.has_key? :id warn "asciidoctor: WARNING:#{reader.nil? ? nil : " #{reader.prev_line_info}:"} multiple ids detected in style attribute" end parsed[type] = collector.join else parsed[type] = collector.join end collector = [] end } raw_style.split('').each do |c| if c == '.' || c == '#' || c == '%' save_current.call case c when '.' type = :role when '#' type = :id when '%' type = :option end else collector.push c end end # small optimization if no shorthand is found if type == :style parsed_style = attributes['style'] = raw_style else save_current.call if parsed.has_key? :style parsed_style = attributes['style'] = parsed[:style] else parsed_style = nil end if parsed.has_key? :id attributes['id'] = parsed[:id] end if parsed.has_key? :role attributes['role'] = parsed[:role] * ' ' end if parsed.has_key? :option (options = parsed[:option]).each do |option| attributes["#{option}-option"] = '' end if (existing_opts = attributes['options']) attributes['options'] = (options + existing_opts.split(',')) * ',' else attributes['options'] = options * ',' end end end [parsed_style, original_style] end end # Remove the indentation (block offset) shared by all the lines, then # indent the lines by the specified amount if specified # # Trim the leading whitespace (indentation) equivalent to the length # of the indent on the least indented line. If the indent argument # is specified, indent the lines by this many spaces (columns). # # The purpose of this method is to shift a block of text to # align to the left margin, while still preserving the relative # indentation between lines # # lines - the Array of String lines to process # indent - the integer number of spaces to add to the beginning # of each line; if this value is nil, the existing # space is preserved (optional, default: 0) # # Examples # # source = < [" def names\n", " @names.split ' '\n", " end\n"] # # Lexer.reset_block_indent(source.lines.entries) # # => ["def names\n", " @names.split ' '\n", "end\n"] # # puts Lexer.reset_block_indent(source.lines.entries).join # # => def names # # => @names.split ' ' # # => end # # returns the Array of String lines with block offset removed #-- # FIXME refactor gsub matchers into compiled regex def self.reset_block_indent!(lines, indent = 0) return if indent.nil? || lines.empty? tab_detected = false # TODO make tab size configurable tab_expansion = ' ' # strip leading block indent offsets = lines.map do |line| # break if the first char is non-whitespace break [] unless line.chomp[0..0].lstrip.empty? if line.include? "\t" tab_detected = true line = line.gsub("\t", tab_expansion) end if (flush_line = line.lstrip).empty? nil elsif (offset = line.length - flush_line.length) == 0 break [] else offset end end unless offsets.empty? || (offsets = offsets.compact).empty? if (offset = offsets.min) > 0 lines.map! {|line| line = line.gsub("\t", tab_expansion) if tab_detected line[offset..-1] || "\n" } end end if indent > 0 padding = ' ' * indent lines.map! {|line| %(#{padding}#{line}) } end nil end # Public: Convert a string to a legal attribute name. # # name - the String name of the attribute # # Returns a String with the legal AsciiDoc attribute name. # # Examples # # sanitize_attribute_name('Foo Bar') # => 'foobar' # # sanitize_attribute_name('foo') # => 'foo' # # sanitize_attribute_name('Foo 3 #-Billy') # => 'foo3-billy' def self.sanitize_attribute_name(name) name.gsub(REGEXP[:illegal_attr_name_chars], '').downcase end # Internal: Converts a Roman numeral to an integer value. # # value - The String Roman numeral to convert # # Returns the Integer for this Roman numeral def self.roman_numeral_to_int(value) value = value.downcase digits = { 'i' => 1, 'v' => 5, 'x' => 10 } result = 0 (0..value.length - 1).each {|i| digit = digits[value[i..i]] if i + 1 < value.length && digits[value[i+1..i+1]] > digit result -= digit else result += digit end } result end end end asciidoctor-0.1.4/lib/asciidoctor/list.rb000066400000000000000000000042371221220517700203750ustar00rootroot00000000000000module Asciidoctor # Public: Methods for managing AsciiDoc lists (ordered, unordered and labeled lists) class List < AbstractBlock # Public: Create alias for blocks alias :items :blocks alias :items? :blocks? def initialize(parent, context) super(parent, context) end # Public: Get the items in this list as an Array def content @blocks end def render result = super @document.callouts.next_list if @context == :colist result end end # Public: Methods for managing items for AsciiDoc olists, ulist, and dlists. class ListItem < AbstractBlock # Public: Get/Set the String used to mark this list item attr_accessor :marker # Public: Initialize an Asciidoctor::ListItem object. # # parent - The parent list block for this list item # text - the String text (default nil) def initialize(parent, text = nil) super(parent, :list_item) @text = text @level = parent.level end def text? !@text.to_s.empty? end def text apply_subs @text end # Public: Fold the first paragraph block into the text # # Here are the rules for when a folding occurs: # # Given: this list item has at least one block # When: the first block is a paragraph that's not connected by a list continuation # Or: the first block is an indented paragraph that's adjacent (wrapped line) # Or: the first block is an indented paragraph that's not connected by a list continuation # Then: then drop the first block and fold it's content (buffer) into the list text # # Returns nothing def fold_first(continuation_connects_first_block = false, content_adjacent = false) if !(first_block = @blocks.first).nil? && first_block.is_a?(Block) && ((first_block.context == :paragraph && !continuation_connects_first_block) || ((content_adjacent || !continuation_connects_first_block) && first_block.context == :literal && first_block.option?('listparagraph'))) block = blocks.shift unless @text.to_s.empty? block.lines.unshift("#@text\n") end @text = block.source end nil end def to_s "#@context [text:#@text, blocks:#{(@blocks || []).size}]" end end end asciidoctor-0.1.4/lib/asciidoctor/path_resolver.rb000066400000000000000000000320001221220517700222640ustar00rootroot00000000000000module Asciidoctor # Public: Handles all operations for resolving, cleaning and joining paths. # This class includes operations for handling both web paths (request URIs) and # system paths. # # The main emphasis of the class is on creating clean and secure paths. Clean # paths are void of duplicate parent and current directory references in the # path name. Secure paths are paths which are restricted from accessing # directories outside of a jail root, if specified. # # Since joining two paths can result in an insecure path, this class also # handles the task of joining a parent (start) and child (target) path. # # This class makes no use of path utilities from the Ruby libraries. Instead, # it handles all aspects of path manipulation. The main benefit of # internalizing these operations is that the class is able to handle both posix # and windows paths independent of the operating system on which it runs. This # makes the class both deterministic and easier to test. # # Examples # # resolver = PathResolver.new # # # Web Paths # # resolver.web_path('images') # => 'images' # # resolver.web_path('./images') # => './images' # # resolver.web_path('/images') # => '/images' # # resolver.web_path('./images/../assets/images') # => './assets/images' # # resolver.web_path('/../images') # => '/images' # # resolver.web_path('images', 'assets') # => 'assets/images' # # resolver.web_path('tiger.png', '../assets/images') # => '../assets/images/tiger.png' # # # System Paths # # resolver.working_dir # => '/path/to/docs' # # resolver.system_path('images') # => '/path/to/docs/images' # # resolver.system_path('../images') # => '/path/to/images' # # resolver.system_path('/etc/images') # => '/etc/images' # # resolver.system_path('images', '/etc') # => '/etc/images' # # resolver.system_path('', '/etc/images') # => '/etc/images' # # resolver.system_path(nil, nil, '/path/to/docs') # => '/path/to/docs' # # resolver.system_path('..', nil, '/path/to/docs') # => '/path/to/docs' # # resolver.system_path('../../../css', nil, '/path/to/docs') # => '/path/to/docs/css' # # resolver.system_path('../../../css', '../../..', '/path/to/docs') # => '/path/to/docs/css' # # resolver.system_path('..', 'C:\\data\\docs\\assets', 'C:\\data\\docs') # => 'C:/data/docs' # # resolver.system_path('..\\..\\css', 'C:\\data\\docs\\assets', 'C:\\data\\docs') # => 'C:/data/docs/css' # # begin # resolver.system_path('../../../css', '../../..', '/path/to/docs', :recover => false) # rescue SecurityError => e # puts e.message # end # => 'path ../../../../../../css refers to location outside jail: /path/to/docs (disallowed in safe mode)' # # resolver.system_path('/path/to/docs/images', nil, '/path/to/docs') # => '/path/to/docs/images' # # begin # resolver.system_path('images', '/etc', '/path/to/docs') # rescue SecurityError => e # puts e.message # end # => Start path /etc is outside of jail: /path/to/docs' # class PathResolver DOT = '.' DOT_DOT = '..' SLASH = '/' BACKSLASH = '\\' WIN_ROOT_RE = /^[[:alpha:]]:(?:\\|\/)/ attr_accessor :file_separator attr_accessor :working_dir # Public: Construct a new instance of PathResolver, optionally specifying the # file separator (to override the system default) and the working directory # (to override the present working directory). The working directory will be # expanded to an absolute path inside the constructor. # # file_separator - the String file separator to use for path operations # (optional, default: File::FILE_SEPARATOR) # working_dir - the String working directory (optional, default: Dir.pwd) # def initialize(file_separator = nil, working_dir = nil) @file_separator = file_separator.nil? ? (File::ALT_SEPARATOR || File::SEPARATOR) : file_separator if working_dir.nil? @working_dir = File.expand_path(Dir.pwd) else @working_dir = is_root?(working_dir) ? working_dir : File.expand_path(working_dir) end end # Public: Check if the specified path is an absolute root path # This operation correctly handles both posix and windows paths. # # path - the String path to check # # returns a Boolean indicating whether the path is an absolute root path def is_root?(path) if @file_separator == BACKSLASH && path.match(WIN_ROOT_RE) true elsif path.start_with? SLASH true else false end end # Public: Determine if the path is an absolute (root) web path # # path - the String path to check # # returns a Boolean indicating whether the path is an absolute (root) web path def is_web_root?(path) path.start_with? SLASH end # Public: Normalize path by converting any backslashes to forward slashes # # path - the String path to normalize # # returns a String path with any backslashes replaced with forward slashes def posixfy(path) return '' if path.to_s.empty? path.include?(BACKSLASH) ? path.tr(BACKSLASH, SLASH) : path end # Public: Expand the path by resolving any parent references (..) # and cleaning self references (.). # # The result will be relative if the path is relative and # absolute if the path is absolute. The file separator used # in the expanded path is the one specified when the class # was constructed. # # path - the String path to expand # # returns a String path with any parent or self references resolved. def expand_path(path) path_segments, path_root, _ = partition_path(path) join_path path_segments, path_root end # Public: Partition the path into path segments and remove any empty segments # or segments that are self references (.). The path is split on either posix # or windows file separators. # # path - the String path to partition # web_path - a Boolean indicating whether the path should be handled # as a web path (optional, default: false) # # returns a 3-item Array containing the Array of String path segments, the # path root, if the path is absolute, and the posix version of the path. def partition_path(path, web_path = false) posix_path = posixfy path is_root = web_path ? is_web_root?(posix_path) : is_root?(posix_path) path_segments = posix_path.tr_s(SLASH, SLASH).split(SLASH) # capture relative root root = path_segments.first == DOT ? DOT : nil path_segments.delete(DOT) # capture absolute root, preserving relative root if set root = is_root ? path_segments.shift : root [path_segments, root, posix_path] end # Public: Join the segments using the posix file separator (since Ruby knows # how to work with paths specified this way, regardless of OS). Use the root, # if specified, to construct an absolute path. Otherwise join the segments as # a relative path. # # segments - a String Array of path segments # root - a String path root (optional, default: nil) # # returns a String path formed by joining the segments using the posix file # separator and prepending the root, if specified def join_path(segments, root = nil) if root "#{root}#{SLASH}#{segments * SLASH}" else segments * SLASH end end # Public: Resolve a system path from the target and start paths. If a jail # path is specified, enforce that the resolved directory is contained within # the jail path. If a jail path is not provided, the resolved path may be # any location on the system. If the resolved path is absolute, use it as is. # If the resolved path is relative, resolve it relative to the working_dir # specified in the constructor. # # target - the String target path # start - the String start (i.e., parent) path # jail - the String jail path to confine the resolved path # opts - an optional Hash of options to control processing (default: {}): # * :recover is used to control whether the processor should auto-recover # when an illegal path is encountered # * :target_name is used in messages to refer to the path being resolved # # returns a String path that joins the target path with the start path with # any parent references resolved and self references removed and enforces # that the resolved path be contained within the jail, if provided def system_path(target, start, jail = nil, opts = {}) recover = opts.fetch(:recover, true) unless jail.nil? unless is_root? jail raise SecurityError, "Jail is not an absolute path: #{jail}" end jail = posixfy jail end if target.to_s.empty? target_segments = [] else target_segments, target_root, _ = partition_path(target) end if target_segments.empty? if start.to_s.empty? return jail.nil? ? @working_dir : jail elsif is_root? start if jail.nil? return expand_path start end else return system_path(start, jail, jail) end end if target_root && target_root != DOT resolved_target = join_path target_segments, target_root # if target is absolute and a sub-directory of jail, or # a jail is not in place, let it slide if jail.nil? || resolved_target.start_with?(jail) return resolved_target end end if start.to_s.empty? start = jail.nil? ? @working_dir : jail elsif is_root? start start = posixfy start else start = system_path(start, jail, jail) end # both jail and start have been posixfied at this point if jail == start jail_segments, jail_root, _ = partition_path(jail) start_segments = jail_segments.dup elsif !jail.nil? if !start.start_with?(jail) raise SecurityError, "#{opts[:target_name] || 'Start path'} #{start} is outside of jail: #{jail} (disallowed in safe mode)" end start_segments, start_root, _ = partition_path(start) jail_segments, jail_root, _ = partition_path(jail) # Already checked for this condition #if start_root != jail_root # raise SecurityError, "Jail root #{jail_root} does not match root of #{opts[:target_name] || 'start path'}: #{start_root}" #end else start_segments, start_root, _ = partition_path(start) jail_root = start_root end resolved_segments = start_segments.dup warned = false target_segments.each do |segment| if segment == DOT_DOT if !jail.nil? if resolved_segments.length > jail_segments.length resolved_segments.pop elsif !recover raise SecurityError, "#{opts[:target_name] || 'path'} #{target} refers to location outside jail: #{jail} (disallowed in safe mode)" elsif !warned warn "asciidoctor: WARNING: #{opts[:target_name] || 'path'} has illegal reference to ancestor of jail, auto-recovering" warned = true end else resolved_segments.pop end else resolved_segments.push segment end end join_path resolved_segments, jail_root end # Public: Resolve a web path from the target and start paths. # The main function of this operation is to resolve any parent # references and remove any self references. # # target - the String target path # start - the String start (i.e., parent) path # # returns a String path that joins the target path with the # start path with any parent references resolved and self # references removed def web_path(target, start = nil) target = posixfy(target) start = posixfy(start) uri_prefix = nil unless is_web_root?(target) || start.empty? target = "#{start}#{SLASH}#{target}" if target.include?(':') && target.match(Asciidoctor::REGEXP[:uri_sniff]) uri_prefix = $~[0] target = target[uri_prefix.length..-1] end end target_segments, target_root, _ = partition_path(target, true) resolved_segments = target_segments.inject([]) do |accum, segment| if segment == DOT_DOT if accum.empty? accum.push segment unless target_root && target_root != DOT elsif accum[-1] == DOT_DOT accum.push segment else accum.pop end else accum.push segment end accum end if uri_prefix.nil? join_path resolved_segments, target_root else "#{uri_prefix}#{join_path resolved_segments, target_root}" end end # Public: Calculate the relative path to this absolute filename from the specified base directory # # If either the filename or the base_directory are not absolute paths, no work is done. # # filename - An absolute file name as a String # base_directory - An absolute base directory as a String # # Return the relative path String of the filename calculated from the base directory def relative_path(filename, base_directory) if (is_root? filename) && (is_root? base_directory) offset = base_directory.chomp(@file_separator).length + 1 filename[offset..-1] else filename end end end end asciidoctor-0.1.4/lib/asciidoctor/reader.rb000066400000000000000000001102241221220517700206560ustar00rootroot00000000000000module Asciidoctor # Public: Methods for retrieving lines from AsciiDoc source files class Reader class Cursor attr_accessor :file attr_accessor :dir attr_accessor :path attr_accessor :lineno def initialize file, dir = nil, path = nil, lineno = nil @file = file @dir = dir @path = path @lineno = lineno end def line_info %(#{path}: line #{lineno}) end end attr_reader :file attr_reader :dir attr_reader :path # Public: Get the 1-based offset of the current line. attr_reader :lineno # Public: Get the document source as a String Array of lines. attr_reader :source_lines # Public: Control whether lines are processed using Reader#process_line on first visit (default: true) attr_accessor :process_lines # Public: Initialize the Reader object def initialize data = nil, cursor = nil if cursor.nil? @file = @dir = nil @path = '' @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call! elsif cursor.is_a? String @file = cursor @dir = File.dirname @file @path = File.basename @file @lineno = 1 # IMPORTANT lineno assignment must proceed prepare_lines call! else @file = cursor.file @dir = cursor.dir @path = cursor.path || '' unless @file.nil? if @dir.nil? # REVIEW might to look at this assignment closer @dir = File.dirname @file @dir = nil if @dir == '.' # right? end if cursor.path.nil? @path = File.basename @file end end @lineno = cursor.lineno || 1 # IMPORTANT lineno assignment must proceed prepare_lines call! end @lines = data.nil? ? [] : (prepare_lines data) @source_lines = @lines.dup @eof = @lines.empty? @look_ahead = 0 @process_lines = true @unescape_next_line = false end # Internal: Prepare the lines from the provided data # # This method strips whitespace from the end of every line of # the source data and appends a LF (i.e., Unix endline). This # whitespace substitution is very important to how Asciidoctor # works. # # Any leading or trailing blank lines are also removed. # # The normalized lines are assigned to the @lines instance variable. # # data - A String Array of input data to be normalized # opts - A Hash of options to control what cleansing is done # # Returns The String lines extracted from the data def prepare_lines data, opts = {} data.is_a?(String) ? data.each_line.to_a : data.dup end # Internal: Processes a previously unvisited line # # By default, this method marks the line as processed # by incrementing the look_ahead counter and returns # the line unmodified. # # Returns The String line the Reader should make available to the next # invocation of Reader#read_line or nil if the Reader should drop the line, # advance to the next line and process it. def process_line line @look_ahead += 1 if @process_lines line end # Public: Check whether there are any lines left to read. # # If a previous call to this method resulted in a value of false, # immediately returned the cached value. Otherwise, delegate to # peek_line to determine if there is a next line available. # # Returns True if there are more lines, False if there are not. def has_more_lines? !(@eof || (@eof = peek_line.nil?)) end # Public: Peek at the next line and check if it's empty (i.e., whitespace only) # # This method Does not consume the line from the stack. # # Returns True if the there are no more lines or if the next line is empty def next_line_empty? (line = peek_line).nil? || line.chomp.empty? end # Public: Peek at the next line of source data. Processes the line, if not # already marked as processed, but does not consume it. # # This method will probe the reader for more lines. If there is a next line # that has not previously been visited, the line is passed to the # Reader#preprocess_line method to be initialized. This call gives # sub-classess the opportunity to do preprocessing. If the return value of # the Reader#process_line is nil, the data is assumed to be changed and # Reader#peek_line is invoked again to perform further processing. # # direct - A Boolean flag to bypasses the check for more lines and immediately # returns the first element of the internal @lines Array. (default: false) # # Returns the next line of the source data as a String if there are lines remaining. # Returns nil if there is no more data. def peek_line direct = false if direct || @look_ahead > 0 @unescape_next_line ? @lines.first[1..-1] : @lines.first elsif @eof || @lines.empty? @eof = true @look_ahead = 0 nil else # FIXME the problem with this approach is that we aren't # retaining the modified line (hence the @unescape_next_line tweak) # perhaps we need a stack of proxy lines if (line = process_line @lines.first).nil? peek_line else line end end end # Public: Peek at the next multiple lines of source data. Processes the lines, if not # already marked as processed, but does not consume them. # # This method delegates to Reader#read_line to process and collect the line, then # restores the lines to the stack before returning them. This allows the lines to # be processed and marked as such so that subsequent reads will not need to process # the lines again. # # num - The Integer number of lines to peek. # direct - A Boolean indicating whether processing should be disabled when reading lines # # Returns A String Array of the next multiple lines of source data, or an empty Array # if there are no more lines in this Reader. def peek_lines num = 1, direct = true old_look_ahead = @look_ahead result = [] (1..num).each do if (line = read_line direct) result << line else break end end unless result.empty? result.reverse_each {|line| unshift line } @look_ahead = old_look_ahead if direct end result end # Public: Get the next line of source data. Consumes the line returned. # # direct - A Boolean flag to bypasses the check for more lines and immediately # returns the first element of the internal @lines Array. (default: false) # # Returns the String of the next line of the source data if data is present. # Returns nil if there is no more data. def read_line direct = false if direct || @look_ahead > 0 || has_more_lines? shift else nil end end # Public: Get the remaining lines of source data. # # This method calls Reader#read_line repeatedly until all lines are consumed # and returns the lines as a String Array. This method differs from # Reader#lines in that it processes each line in turn, hence triggering # any preprocessors implemented in sub-classes. # # Returns the lines read as a String Array def read_lines lines = [] while has_more_lines? lines << read_line end lines end alias :readlines :read_lines # Public: Get the remaining lines of source data joined as a String. # # Delegates to Reader#read_lines, then joins the result. # # Returns the lines read joined as a String def read read_lines.join end # Public: Advance to the next line by discarding the line at the front of the stack # # direct - A Boolean flag to bypasses the check for more lines and immediately # returns the first element of the internal @lines Array. (default: true) # # returns a Boolean indicating whether there was a line to discard. def advance direct = true !(read_line direct).nil? end # Public: Push the String line onto the beginning of the Array of source data. # # Since this line was (assumed to be) previously retrieved through the # reader, it is marked as seen. # # returns nil def unshift_line line_to_restore unshift line_to_restore nil end alias :restore_line :unshift_line # Public: Push an Array of lines onto the front of the Array of source data. # # Since these lines were (assumed to be) previously retrieved through the # reader, they are marked as seen. # # Returns nil def unshift_lines lines_to_restore # QUESTION is it faster to use unshift(*lines_to_restore)? lines_to_restore.reverse_each {|line| unshift line } nil end alias :restore_lines :unshift_lines # Public: Replace the current line with the specified line. # # Calls Reader#advance to consume the current line, then calls # Reader#unshift to push the replacement onto the top of the # line stack. # # replacement - The String line to put in place of the line at the cursor. # # Returns nothing. def replace_line replacement advance unshift replacement nil end # Public: Strip off leading blank lines in the Array of lines. # # Examples # # @lines # => ["\n", "\t\n", "Foo\n", "Bar\n", "\n"] # # skip_blank_lines # => 2 # # @lines # => ["Foo\n", "Bar\n"] # # Returns an Integer of the number of lines skipped def skip_blank_lines return 0 if eof? num_skipped = 0 # optimized code for shortest execution path while (next_line = peek_line) if next_line.chomp.empty? advance num_skipped += 1 else return num_skipped end end num_skipped end # Public: Skip consecutive lines containing line comments and return them. # # Examples # @lines # => ["// foo\n", "bar\n"] # # comment_lines = skip_comment_lines # => ["// foo\n"] # # @lines # => ["bar\n"] # # Returns the Array of lines that were skipped def skip_comment_lines opts = {} return [] if eof? comment_lines = [] include_blank_lines = opts[:include_blank_lines] while (next_line = peek_line) if include_blank_lines && next_line.chomp.empty? comment_lines << read_line elsif (commentish = next_line.start_with?('//')) && (match = next_line.match(REGEXP[:comment_blk])) comment_lines << read_line comment_lines.push(*(read_lines_until(:terminator => match[0], :read_last_line => true, :skip_processing => true))) elsif commentish && next_line.match(REGEXP[:comment]) comment_lines << read_line else break end end comment_lines end # Public: Skip consecutive lines that are line comments and return them. def skip_line_comments return [] if eof? comment_lines = [] # optimized code for shortest execution path while (next_line = peek_line) if next_line.match(REGEXP[:comment]) comment_lines << read_line else break end end comment_lines end # Public: Advance to the end of the reader, consuming all remaining lines # # Returns nothing. def terminate @lineno += @lines.size @lines.clear @eof = true @look_ahead = 0 nil end # Public: Check whether this reader is empty (contains no lines) # # Returns true if there are no more lines to peek, otherwise false. def eof? !has_more_lines? end alias :empty? :eof? # Public: Return all the lines from `@lines` until we (1) run out them, # (2) find a blank line with :break_on_blank_lines => true, or (3) find # a line for which the given block evals to true. # # options - an optional Hash of processing options: # * :break_on_blank_lines may be used to specify to break on # blank lines # * :skip_first_line may be used to tell the reader to advance # beyond the first line before beginning the scan # * :preserve_last_line may be used to specify that the String # causing the method to stop processing lines should be # pushed back onto the `lines` Array. # * :read_last_line may be used to specify that the String # causing the method to stop processing lines should be # included in the lines being returned # # Returns the Array of lines forming the next segment. # # Examples # # reader = Reader.new ["First paragraph\n", "Second paragraph\n", # "Open block\n", "\n", "Can have blank lines\n", # "--\n", "\n", "In a different segment\n"] # # reader.read_lines_until # => ["First paragraph\n", "Second paragraph\n", "Open block\n"] def read_lines_until options = {} result = [] advance if options[:skip_first_line] if @process_lines && options[:skip_processing] @process_lines = false restore_process_lines = true else restore_process_lines = false end has_block = block_given? if (terminator = options[:terminator]) break_on_blank_lines = false break_on_list_continuation = false chomp_last_line = options.fetch :chomp_last_line, false else break_on_blank_lines = options[:break_on_blank_lines] break_on_list_continuation = options[:break_on_list_continuation] chomp_last_line = break_on_blank_lines end skip_line_comments = options[:skip_line_comments] line_read = false line_restored = false while (line = read_line) finish = while true break true if terminator && line.chomp == terminator # QUESTION: can we get away with line.chomp.empty? here? break true if break_on_blank_lines && line.chomp.empty? if break_on_list_continuation && line_read && line.chomp == LIST_CONTINUATION options[:preserve_last_line] = true break true end break true if has_block && (yield line) break false end if finish if options[:read_last_line] result << line line_read = true end if options[:preserve_last_line] restore_line line line_restored = true end break end unless skip_line_comments && line.start_with?('//') && line.match(REGEXP[:comment]) result << line line_read = true end end if chomp_last_line && line_read result << result.pop.chomp end if restore_process_lines @process_lines = true @look_ahead -= 1 if line_restored && terminator.nil? end result end # Internal: Shift the line off the stack and increment the lineno def shift @lineno += 1 @look_ahead -= 1 unless @look_ahead == 0 @lines.shift end # Internal: Restore the line to the stack and decrement the lineno def unshift line @lineno -= 1 @look_ahead += 1 @eof = false @lines.unshift line end def cursor Cursor.new @file, @dir, @path, @lineno end # Public: Get information about the last line read, including file name and line number. # # Returns A String summary of the last line read def line_info %(#{@path}: line #{@lineno}) end alias :next_line_info :line_info def prev_line_info %(#{@path}: line #{@lineno - 1}) end # Public: Get a copy of the remaining Array of String lines managed by this Reader # # Returns A copy of the String Array of lines remaining in this Reader def lines @lines.dup end # Public: Get a copy of the remaining lines managed by this Reader joined as a String def string @lines.join end # Public: Get the source lines for this Reader joined as a String def source @source_lines.join end # Public: Get a summary of this Reader. # # # Returns A string summary of this reader, which contains the path and line information def to_s line_info end end # Public: Methods for retrieving lines from AsciiDoc source files, evaluating preprocessor # directives as each line is read off the Array of lines. class PreprocessorReader < Reader attr_reader :include_stack attr_reader :includes # Public: Initialize the PreprocessorReader object def initialize document, data = nil, cursor = nil @document = document super data, cursor include_depth_default = document.attributes.fetch('max-include-depth', 64).to_i include_depth_default = 0 if include_depth_default < 0 # track both absolute depth for comparing to size of include stack and relative depth for reporting @maxdepth = {:abs => include_depth_default, :rel => include_depth_default} @include_stack = [] @includes = (document.references[:includes] ||= []) @skipping = false @conditional_stack = [] @include_processors = nil end def prepare_lines data, opts = {} if data.is_a?(String) if ::Asciidoctor::FORCE_ENCODING result = data.each_line.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" } else result = data.each_line.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" } end else if ::Asciidoctor::FORCE_ENCODING result = data.map {|line| "#{line.rstrip.force_encoding ::Encoding::UTF_8}#{::Asciidoctor::EOL}" } else result = data.map {|line| "#{line.rstrip}#{::Asciidoctor::EOL}" } end end # QUESTION should this work for AsciiDoc table cell content? Currently it does not. unless @document.nil? || !(@document.attributes.has_key? 'skip-front-matter') if (front_matter = skip_front_matter! result) @document.attributes['front-matter'] = front_matter.join.chomp end end # QUESTION should we chomp last line? (with or without the condense flag?) if opts.fetch(:condense, true) result.shift && @lineno += 1 while !(first = result.first).nil? && first == ::Asciidoctor::EOL result.pop while !(last = result.last).nil? && last == ::Asciidoctor::EOL end if (indent = opts.fetch(:indent, nil)) Lexer.reset_block_indent! result, indent.to_i end result end def process_line line return line unless @process_lines if line.chomp.empty? @look_ahead += 1 return '' end macroish = line.include?('::') && line.include?('[') if macroish && line.include?('if') && (match = line.match(REGEXP[:ifdef_macro])) # if escaped, mark as processed and return line unescaped if line.start_with? '\\' @unescape_next_line = true @look_ahead += 1 line[1..-1] else if preprocess_conditional_inclusion(*match.captures) # move the pointer past the conditional line advance # treat next line as uncharted territory nil else # the line was not a valid conditional line # mark it as visited and return it @look_ahead += 1 line end end elsif @skipping advance nil elsif macroish && line.include?('include::') && (match = line.match(REGEXP[:include_macro])) # if escaped, mark as processed and return line unescaped if line.start_with? '\\' @unescape_next_line = true @look_ahead += 1 line[1..-1] else # QUESTION should we strip whitespace from raw attributes in Substituters#parse_attributes? (check perf) if preprocess_include match[1], match[2].strip # peek again since the content has changed nil else # the line was not a valid include line and is unchanged # mark it as visited and return it @look_ahead += 1 line end end else # optimization to inline super #super @look_ahead += 1 line end end # Public: Override the Reader#peek_line method to pop the include # stack if the last line has been reached and there's at least # one include on the stack. # # Returns the next line of the source data as a String if there are lines remaining # in the current include context or a parent include context. # Returns nil if there are no more lines remaining and the include stack is empty. def peek_line direct = false if (line = super) line elsif @include_stack.empty? nil else pop_include peek_line direct end end # Internal: Preprocess the directive (macro) to conditionally include content. # # Preprocess the conditional inclusion directive (ifdef, ifndef, ifeval, # endif) under the cursor. If the Reader is currently skipping content, then # simply track the open and close delimiters of any nested conditional # blocks. If the Reader is not skipping, mark whether the condition is # satisfied and continue preprocessing recursively until the next line of # available content is found. # # directive - The conditional inclusion directive (ifdef, ifndef, ifeval, endif) # target - The target, which is the name of one or more attributes that are # used in the condition (blank in the case of the ifeval directive) # delimiter - The conditional delimiter for multiple attributes ('+' means all # attributes must be defined or undefined, ',' means any of the attributes # can be defined or undefined. # text - The text associated with this directive (occurring between the square brackets) # Used for a single-line conditional block in the case of the ifdef or # ifndef directives, and for the conditional expression for the ifeval directive. # # returns a Boolean indicating whether the cursor should be advanced def preprocess_conditional_inclusion directive, target, delimiter, text # must have a target before brackets if ifdef or ifndef # must not have text between brackets if endif # don't honor match if it doesn't meet this criteria # QUESTION should we warn for these bogus declarations? if ((directive == 'ifdef' || directive == 'ifndef') && target.empty?) || (directive == 'endif' && !text.nil?) return false end if directive == 'endif' stack_size = @conditional_stack.size if stack_size > 0 pair = @conditional_stack.last if target.empty? || target == pair[:target] @conditional_stack.pop @skipping = @conditional_stack.empty? ? false : @conditional_stack.last[:skipping] else warn "asciidoctor: ERROR: #{line_info}: mismatched macro: endif::#{target}[], expected endif::#{pair[:target]}[]" end else warn "asciidoctor: ERROR: #{line_info}: unmatched macro: endif::#{target}[]" end return true end skip = false unless @skipping # QUESTION any way to wrap ifdef & ifndef logic up together? case directive when 'ifdef' case delimiter when nil # if the attribute is undefined, then skip skip = !@document.attributes.has_key?(target) when ',' # if any attribute is defined, then don't skip skip = !target.split(',').detect {|name| @document.attributes.has_key? name } when '+' # if any attribute is undefined, then skip skip = target.split('+').detect {|name| !@document.attributes.has_key? name } end when 'ifndef' case delimiter when nil # if the attribute is defined, then skip skip = @document.attributes.has_key?(target) when ',' # if any attribute is undefined, then don't skip skip = !target.split(',').detect {|name| !@document.attributes.has_key? name } when '+' # if any attribute is defined, then skip skip = target.split('+').detect {|name| @document.attributes.has_key? name } end when 'ifeval' # the text in brackets must match an expression # don't honor match if it doesn't meet this criteria if !target.empty? || !(expr_match = text.strip.match(REGEXP[:eval_expr])) return false end lhs = resolve_expr_val expr_match[1] # regex enforces a restrict set of math-related operations op = expr_match[2] rhs = resolve_expr_val expr_match[3] skip = !(lhs.send op.to_sym, rhs) end end # conditional inclusion block if directive == 'ifeval' || text.nil? @skipping = true if skip @conditional_stack << {:target => target, :skip => skip, :skipping => @skipping} # single line conditional inclusion else unless @skipping || skip # FIXME slight hack to skip past conditional line # but keep our synthetic line marked as processed conditional_line = peek_line true replace_line "#{text.rstrip}#{::Asciidoctor::EOL}" unshift conditional_line return true end end true end # Internal: Preprocess the directive (macro) to include the target 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 name of the source document to include as specified in the # target slot of the include::[] macro # # returns a Boolean indicating whether the line under the cursor has changed. def preprocess_include target, raw_attributes target = @document.sub_attributes target, :attribute_missing => 'drop-line' if target.empty? if @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing]) == 'skip' false else advance true end # assume that if an include processor is given, the developer wants # to handle when and how to process the include elsif include_processors? && (processor = @include_processors.find {|candidate| candidate.handles? target }) advance # QUESTION should we use @document.parse_attribues? processor.process self, target, AttributeList.new(raw_attributes).parse 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 @document.safe >= SafeMode::SECURE replace_line "link:#{target}[]#{::Asciidoctor::EOL}" # TODO make creating the output target a helper method #output_target = %(#{File.join(File.dirname(target), File.basename(target, File.extname(target)))}#{@document.attributes['outfilesuffix']}) #unshift "link:#{output_target}[]#{::Asciidoctor::EOL}" true elsif (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth warn %(asciidoctor: ERROR: #{line_info}: maximum include depth of #{@maxdepth[:rel]} exceeded) false elsif abs_maxdepth > 0 if target.include?(':') && target.match(REGEXP[:uri_sniff]) unless @document.attributes.has_key? 'allow-uri-read' replace_line "link:#{target}[]#{::Asciidoctor::EOL}" return true end target_type = :uri include_file = path = target if @document.attributes.has_key? 'cache-uri' # caching requires the open-uri-cached gem to be installed # processing will be automatically aborted if these libraries can't be opened Helpers.require_library 'open-uri/cached', 'open-uri-cached' else Helpers.require_library 'open-uri' end else target_type = :file # include file is resolved relative to dir of current include, or base_dir if within original docfile include_file = @document.normalize_system_path(target, @dir, nil, :target_name => 'include file') if !File.file?(include_file) warn "asciidoctor: WARNING: #{line_info}: include file not found: #{include_file}" advance return true end #path = @document.relative_path include_file path = PathResolver.new.relative_path include_file, @document.base_dir end inc_lines = nil tags = nil attributes = {} if !raw_attributes.empty? # QUESTION should we use @document.parse_attribues? attributes = AttributeList.new(raw_attributes).parse if attributes.has_key? 'lines' inc_lines = [] attributes['lines'].split(REGEXP[:ssv_or_csv_delim]).each do |linedef| if linedef.include?('..') from, to = linedef.split('..').map(&:to_i) if to == -1 inc_lines << from inc_lines << 1.0/0.0 else inc_lines.concat Range.new(from, to).to_a end else inc_lines << linedef.to_i end end inc_lines = inc_lines.sort.uniq elsif attributes.has_key? 'tag' tags = [attributes['tag']] elsif attributes.has_key? 'tags' tags = attributes['tags'].split(REGEXP[:ssv_or_csv_delim]).uniq end end if !inc_lines.nil? if !inc_lines.empty? selected = [] inc_line_offset = 0 inc_lineno = 0 begin open(include_file) do |f| f.each_line do |l| inc_lineno += 1 take = inc_lines.first if take.is_a?(Float) && take.infinite? selected.push l inc_line_offset = inc_lineno if inc_line_offset == 0 else if f.lineno == take selected.push l inc_line_offset = inc_lineno if inc_line_offset == 0 inc_lines.shift end break if inc_lines.empty? end end end rescue warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" advance return true end advance # FIXME not accounting for skipped lines in reader line numbering push_include selected, include_file, path, inc_line_offset, attributes end elsif !tags.nil? if !tags.empty? selected = [] inc_line_offset = 0 inc_lineno = 0 active_tag = nil begin open(include_file) do |f| f.each_line do |l| inc_lineno += 1 # must force encoding here since we're performing String operations on line l.force_encoding(::Encoding::UTF_8) if ::Asciidoctor::FORCE_ENCODING if !active_tag.nil? if l.include?("end::#{active_tag}[]") active_tag = nil else selected.push l inc_line_offset = inc_lineno if inc_line_offset == 0 end else tags.each do |tag| if l.include?("tag::#{tag}[]") active_tag = tag break end end end end end rescue warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" advance return true end advance # FIXME not accounting for skipped lines in reader line numbering push_include selected, include_file, path, inc_line_offset, attributes end else begin advance push_include open(include_file) {|f| f.read }, include_file, path, 1, attributes rescue warn "asciidoctor: WARNING: #{line_info}: include #{target_type} not readable: #{include_file}" advance return true end end true else false end end def push_include data, file = nil, path = nil, lineno = 1, attributes = {} @include_stack << [@lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines] @includes << Helpers.rootname(path) @file = file @dir = File.dirname file @path = path @lineno = lineno # NOTE only process lines in AsciiDoc files @process_lines = ASCIIDOC_EXTENSIONS[File.extname(@file)] if attributes.has_key? 'depth' depth = attributes['depth'].to_i depth = 1 if depth <= 0 @maxdepth = {:abs => (@include_stack.size - 1) + depth, :rel => depth} end # effectively fill the buffer @lines = prepare_lines data, :condense => false, :indent => attributes['indent'] # FIXME kind of a hack #Document::AttributeEntry.new('infile', @file).save_to_next_block @document #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document if @lines.empty? pop_include else @eof = false @look_ahead = 0 end end def pop_include if @include_stack.size > 0 @lines, @file, @dir, @path, @lineno, @maxdepth, @process_lines = @include_stack.pop # FIXME kind of a hack #Document::AttributeEntry.new('infile', @file).save_to_next_block @document #Document::AttributeEntry.new('indir', File.dirname(@file)).save_to_next_block @document @eof = @lines.empty? @look_ahead = 0 end end def include_depth @include_stack.size end def exceeded_max_depth? if (abs_maxdepth = @maxdepth[:abs]) > 0 && @include_stack.size >= abs_maxdepth @maxdepth[:rel] else false end end # TODO Document this override # also, we now have the field in the super class, so perhaps # just implement the logic there? def shift if @unescape_next_line @unescape_next_line = false super[1..-1] else super end end # Private: Ignore front-matter, commonly used in static site generators def skip_front_matter! data, increment_linenos = true front_matter = nil if data.size > 0 && data.first.chomp == '---' original_data = data.dup front_matter = [] data.shift @lineno += 1 if increment_linenos while !data.empty? && data.first.chomp != '---' front_matter.push data.shift @lineno += 1 if increment_linenos end if data.empty? data.unshift(*original_data) @lineno = 0 if increment_linenos front_matter = nil else data.shift @lineno += 1 if increment_linenos end end front_matter end # Private: Resolve the value of one side of the expression # # Examples # # expr = '"value"' # resolve_expr_val(expr) # # => "value" # # expr = '"value' # resolve_expr_val(expr) # # => "\"value" # # expr = '"{undefined}"' # resolve_expr_val(expr) # # => "" # # expr = '{undefined}' # resolve_expr_val(expr) # # => nil # # expr = '2' # resolve_expr_val(expr) # # => 2 # # @document.attributes['name'] = 'value' # expr = '"{name}"' # resolve_expr_val(expr) # # => "value" # # Returns The value of the expression, coerced to the appropriate type def resolve_expr_val(str) val = str type = nil if val.start_with?('"') && val.end_with?('"') || val.start_with?('\'') && val.end_with?('\'') type = :string val = val[1...-1] end # QUESTION should we substitute first? if val.include? '{' val = @document.sub_attributes val end unless type == :string if val.empty? val = nil elsif val.strip.empty? val = ' ' elsif val == 'true' val = true elsif val == 'false' val = false elsif val.include?('.') val = val.to_f else # fallback to coercing to integer, since we # require string values to be explicitly quoted val = val.to_i end end val end def include_processors? if @include_processors.nil? if @document.extensions? && @document.extensions.include_processors? @include_processors = @document.extensions.load_include_processors(@document) true else @include_processors = false false end else @include_processors != false end end def to_s %(#{self.class.name} [path: #{@path}, line #: #{@lineno}, include depth: #{@include_stack.size}, include stack: [#{@include_stack.map {|inc| inc.to_s}.join ', '}]]) end end end asciidoctor-0.1.4/lib/asciidoctor/renderer.rb000066400000000000000000000200041221220517700212160ustar00rootroot00000000000000module Asciidoctor # Public: Methods for rendering Asciidoc Documents, Sections, and Blocks # using eRuby templates. class Renderer RE_ASCIIDOCTOR_NAMESPACE = /^Asciidoctor::/ RE_TEMPLATE_CLASS_SUFFIX = /Template$/ RE_CAMELCASE_BOUNDARY_1 = /([[:upper:]]+)([[:upper:]][[:alpha:]])/ RE_CAMELCASE_BOUNDARY_2 = /([[:lower:]])([[:upper:]])/ attr_reader :compact attr_reader :cache @@global_cache = nil # Public: Initialize an Asciidoctor::Renderer object. # def initialize(options={}) @debug = !!options[:debug] @views = {} @compact = options[:compact] @cache = nil backend = options[:backend] case backend when 'html5', 'docbook45', 'docbook5' eruby = load_eruby options[:eruby] #Helpers.require_library 'asciidoctor/backends/' + backend require 'asciidoctor/backends/' + backend # Load up all the template classes that we know how to render for this backend BaseTemplate.template_classes.each do |tc| if tc.to_s.downcase.include?('::' + backend + '::') # optimization view_name, view_backend = self.class.extract_view_mapping(tc) if view_backend == backend @views[view_name] = tc.new(view_name, backend, eruby) end end end else Debug.debug { "No built-in templates for backend: #{backend}" } end # If user passed in a template dir, let them override our base templates if (template_dirs = options.delete(:template_dirs)) Helpers.require_library 'tilt', true if (template_cache = options[:template_cache]) === true # FIXME probably want to use our own cache object for more control @cache = (@@global_cache ||= TemplateCache.new) elsif template_cache @cache = template_cache end view_opts = { :erb => { :trim => '<>' }, :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false }, :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false } } # workaround until we have a proper way to configure if {'html5' => true, 'dzslides' => true, 'deckjs' => true, 'revealjs' => true}.has_key? backend view_opts[:haml][:format] = view_opts[:slim][:format] = :html5 end slim_loaded = false path_resolver = PathResolver.new engine = options[:template_engine] template_dirs.each do |template_dir| # TODO need to think about safe mode restrictions here template_dir = path_resolver.system_path template_dir, nil template_glob = '*' if engine template_glob = "*.#{engine}" # example: templates/haml if File.directory? File.join(template_dir, engine) template_dir = File.join template_dir, engine end end # example: templates/html5 or templates/haml/html5 if File.directory? File.join(template_dir, backend) template_dir = File.join template_dir, backend end # skip scanning folder if we've already done it for same backend/engine if @cache && @cache.cached?(:scan, template_dir, template_glob) @views.update(@cache.fetch :scan, template_dir, template_glob) next end helpers = nil scan_result = {} # Grab the files in the top level of the directory (we're not traversing) Dir.glob(File.join(template_dir, template_glob)). select{|f| File.file? f }.each do |template| basename = File.basename(template) if basename == 'helpers.rb' helpers = template next end name_parts = basename.split('.') next if name_parts.size < 2 view_name = name_parts.first ext_name = name_parts.last if ext_name == 'slim' && !slim_loaded # slim doesn't get loaded by Tilt Helpers.require_library 'slim', true end next unless Tilt.registered? ext_name opts = view_opts[ext_name.to_sym] if @cache @views[view_name] = scan_result[view_name] = @cache.fetch(:view, template) { Tilt.new(template, nil, opts) } else @views[view_name] = Tilt.new template, nil, opts end end require helpers unless helpers.nil? @cache.store(scan_result, :scan, template_dir, template_glob) if @cache end end end # Public: Render an Asciidoc object with a specified view template. # # view - the String view template name. # object - the Object to be used as an evaluation scope. # locals - the optional Hash of locals to be passed to Tilt (default {}) (also ignored, really) def render(view, object, locals = {}) if !@views.has_key? view raise "Couldn't find a view in @views for #{view}" end @views[view].render(object, locals) end def views readonly_views = @views.dup readonly_views.freeze readonly_views end def register_view(view_name, tilt_template) # TODO need to figure out how to cache this @views[view_name] = tilt_template end # Internal: Load the eRuby implementation # # name - the String name of the eRuby implementation (default: 'erb') # # returns the eRuby implementation class def load_eruby(name) if name.nil? || !['erb', 'erubis'].include?(name) name = 'erb' end if name == 'erb' Helpers.require_library 'erb' ::ERB elsif name == 'erubis' Helpers.require_library 'erubis', true ::Erubis::FastEruby end end # TODO better name for this method (and/or field) def self.global_cache @@global_cache end # TODO better name for this method (and/or field) def self.reset_global_cache @@global_cache.clear if @@global_cache end # Internal: Extracts the view name and backend from a qualified Ruby class # # The purpose of this method is to determine the view name and backend to # which a built-in template class maps. We can make certain assumption since # we have control over these class names. The Asciidoctor:: prefix and # Template suffix are stripped as the first step in the conversion. # # qualified_class - The Class or String qualified class name from which to extract the view name and backend # # Examples # # Renderer.extract_view_mapping(Asciidoctor::HTML5::DocumentTemplate) # # => ['document', 'html5'] # # Renderer.extract_view_mapping(Asciidoctor::DocBook45::BlockSidebarTemplate) # # => ['block_sidebar', 'docbook45'] # # Returns A two-element String Array mapped as [view_name, backend], where backend may be nil def self.extract_view_mapping(qualified_class) view_name, backend = qualified_class.to_s. sub(RE_ASCIIDOCTOR_NAMESPACE, ''). sub(RE_TEMPLATE_CLASS_SUFFIX, ''). split('::').reverse view_name = camelcase_to_underscore(view_name) backend = backend.downcase unless backend.nil? [view_name, backend] end # Internal: Convert a CamelCase word to an underscore-delimited word # # Examples # # Renderer.camelcase_to_underscore('BlockSidebar') # # => 'block_sidebar' # # Renderer.camelcase_to_underscore('BlockUlist') # # => 'block_ulist' # # Returns the String converted from CamelCase to underscore-delimited def self.camelcase_to_underscore(str) str.gsub(RE_CAMELCASE_BOUNDARY_1, '\1_\2'). gsub(RE_CAMELCASE_BOUNDARY_2, '\1_\2').downcase end end class TemplateCache attr_reader :cache def initialize @cache = {} end # check if a key is available in the cache def cached? *key @cache.has_key? key end # retrieves an item from the cache stored in the cache key # if a block is given, the block is called and the return # value stored in the cache under the specified key def fetch(*key) if block_given? @cache[key] ||= yield else @cache[key] end end # stores an item in the cache under the specified key def store(value, *key) @cache[key] = value end # Clears the cache def clear @cache = {} end end end asciidoctor-0.1.4/lib/asciidoctor/section.rb000066400000000000000000000120771221220517700210670ustar00rootroot00000000000000module Asciidoctor # Public: Methods for managing sections of AsciiDoc content in a document. # The section responds as an Array of content blocks by delegating # block-related methods to its @blocks Array. # # Examples # # section = Asciidoctor::Section.new # section.title = 'Section 1' # section.id = 'sect1' # # section.size # => 0 # # section.id # => "sect1" # # section << new_block # section.size # => 1 class Section < AbstractBlock # Public: Get/Set the 0-based index order of this section within the parent block attr_accessor :index # Public: Get/Set the number of this section within the parent block # Only relevant if the attribute numbered is true attr_accessor :number # Public: Get/Set the section name of this section attr_accessor :sectname # Public: Get/Set the flag to indicate whether this is a special section or a child of one attr_accessor :special # Public: Get the state of the numbered attribute at this section (need to preserve for creating TOC) attr_accessor :numbered # Public: Initialize an Asciidoctor::Section object. # # parent - The parent Asciidoc Object. def initialize(parent = nil, level = nil, numbered = true) super(parent, :section) @template_name = 'section' if level.nil? if !parent.nil? @level = parent.level + 1 elsif @level.nil? @level = 1 end else @level = level end @numbered = numbered && @level > 0 && @level < 4 @special = parent.is_a?(Section) && parent.special @index = 0 @number = 1 end # Public: The name of this section, an alias of the section title alias :name :title # Public: Generate a String id for this section. # # The generated id is prefixed with value of the 'idprefix' attribute, which # is an underscore by default. # # Section id synthesis can be disabled by undefining the 'sectids' attribute. # # If the generated id is already in use in the document, a count is appended # until a unique id is found. # # Examples # # section = Section.new(parent) # section.title = "Foo" # section.generate_id # => "_foo" # # another_section = Section.new(parent) # another_section.title = "Foo" # another_section.generate_id # => "_foo_1" # # yet_another_section = Section.new(parent) # yet_another_section.title = "Ben & Jerry" # yet_another_section.generate_id # => "_ben_jerry" def generate_id if @document.attributes.has_key? 'sectids' sep = @document.attributes['idseparator'] || '_' pre = @document.attributes['idprefix'] || '_' base_id = %(#{pre}#{title.downcase.gsub(REGEXP[:illegal_sectid_chars], sep).tr_s(sep, sep).chomp(sep)}) # ensure id doesn't begin with idprefix if requested it doesn't if pre.empty? && base_id.start_with?(sep) base_id = base_id[1..-1] base_id = base_id[1..-1] while base_id.start_with?(sep) end gen_id = base_id cnt = 2 while @document.references[:ids].has_key? gen_id gen_id = "#{base_id}#{sep}#{cnt}" cnt += 1 end gen_id else nil end end # Public: Get the section number for the current Section # # The section number is a unique, dot separated String # where each entry represents one level of nesting and # the value of each entry is the 1-based outline number # of the Section amongst its numbered sibling Sections # # delimiter - the delimiter to separate the number for each level # append - the String to append at the end of the section number # or Boolean to indicate the delimiter should not be # appended to the final level # (default: nil) # # Examples # # sect1 = Section.new(document) # sect1.level = 1 # sect1_1 = Section.new(sect1) # sect1_1.level = 2 # sect1_2 = Section.new(sect1) # sect1_2.level = 2 # sect1 << sect1_1 # sect1 << sect1_2 # sect1_1_1 = Section.new(sect1_1) # sect1_1_1.level = 3 # sect1_1 << sect1_1_1 # # sect1.sectnum # # => 1. # # sect1_1.sectnum # # => 1.1. # # sect1_2.sectnum # # => 1.2. # # sect1_1_1.sectnum # # => 1.1.1. # # sect1_1_1.sectnum(',', false) # # => 1,1,1 # # Returns the section number as a String def sectnum(delimiter = '.', append = nil) append ||= (append == false ? '' : delimiter) if !@level.nil? && @level > 1 && @parent.is_a?(Section) "#{@parent.sectnum(delimiter)}#{@number}#{append}" else "#{@number}#{append}" end end # Public: Append a content block to this block's list of blocks. # # If the child block is a Section, assign an index to it. # # block - The child Block to append to this parent Block # # Returns nothing. def <<(block) super if block.context == :section assign_index block end end def to_s if @title if @numbered %[#{super.to_s} - #{sectnum} #@title [blocks:#{@blocks.size}]] else %[#{super.to_s} - #@title [blocks:#{@blocks.size}]] end else super.to_s end end end end asciidoctor-0.1.4/lib/asciidoctor/substituters.rb000066400000000000000000001062741221220517700222060ustar00rootroot00000000000000module Asciidoctor # Public: Methods to perform substitutions on lines of AsciiDoc text. This module # is intented to be mixed-in to Section and Block to provide operations for performing # the necessary substitutions. module Substituters SUBS = { :basic => [:specialcharacters], :normal => [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], :verbatim => [:specialcharacters, :callouts], :title => [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements], :header => [:specialcharacters, :attributes], :pass => [:attributes, :macros] } COMPOSITE_SUBS = { :none => [], :normal => SUBS[:normal], :verbatim => SUBS[:verbatim] } SUB_OPTIONS = { :block => COMPOSITE_SUBS.keys + SUBS[:normal] + [:callouts], :inline => COMPOSITE_SUBS.keys + SUBS[:normal] } # Internal: A String Array of passthough (unprocessed) text captured from this block attr_reader :passthroughs # Public: Apply the specified substitutions to the lines of text # # source - The String or String Array of text to process # subs - The substitutions to perform. Can be a Symbol or a Symbol Array (default: :normal) # expand - A Boolean to control whether sub aliases are expanded (default: true) # # returns Either a String or String Array, whichever matches the type of the first argument def apply_subs source, subs = :normal, expand = false if subs == :normal subs = SUBS[:normal] elsif subs.nil? return source elsif expand if subs.is_a? Symbol subs = COMPOSITE_SUBS[subs] || [subs] else effective_subs = [] subs.each do |key| if COMPOSITE_SUBS.has_key? key effective_subs.push(*COMPOSITE_SUBS[key]) else effective_subs << key end end subs = effective_subs end end return source if subs.empty? multiline = source.is_a?(Array) text = multiline ? source.join : source if (has_passthroughs = subs.include?(:macros)) text = extract_passthroughs(text) end subs.each do |type| case type when :specialcharacters text = sub_specialcharacters(text) when :quotes text = sub_quotes(text) when :attributes text = sub_attributes(text.lines.entries).join when :replacements text = sub_replacements(text) when :macros text = sub_macros(text) when :highlight text = highlight_source(text, subs.include?(:callouts)) when :callouts text = sub_callouts(text) unless subs.include?(:highlight) when :post_replacements text = sub_post_replacements(text) else warn "asciidoctor: WARNING: unknown substitution type #{type}" end end text = restore_passthroughs(text) if has_passthroughs multiline ? text.lines.entries : text end # Public: Apply normal substitutions. # # lines - The lines of text to process. Can be a String or a String Array # # returns - A String with normal substitutions performed def apply_normal_subs(lines) apply_subs lines.is_a?(Array) ? lines.join : lines end # Public: Apply substitutions for titles. # # title - The String title to process # # returns - A String with title substitutions performed def apply_title_subs(title) apply_subs title, SUBS[:title] end # Public: Apply substitutions for header metadata and attribute assignments # # text - String containing the text process # # returns - A String with header substitutions performed def apply_header_subs(text) apply_subs text, SUBS[:header] end =begin # Public: Apply explicit substitutions, if specified, otherwise normal substitutions. # # lines - The lines of text to process. Can be a String or a String Array # # returns - A String with substitutions applied def apply_para_subs(lines) if (subs = attr('subs', nil, false)) apply_subs lines.join, resolve_subs(subs) else apply_subs lines.join end end # Public: Apply substitutions for titles # # lines - A String Array containing the lines of text process # # returns - A String with literal (verbatim) substitutions performed def apply_literal_subs(lines) if (subs = attr('subs', nil, false)) apply_subs lines.join, resolve_subs(subs) elsif @style == 'source' && @document.attributes['basebackend'] == 'html' && ((highlighter = @document.attributes['source-highlighter']) == 'coderay' || highlighter == 'pygments') && attr?('language') highlight_source lines.join, highlighter, callouts else apply_subs lines.join, SUBS[:verbatim] end end # Public: Apply substitutions for passthrough text # # lines - A String Array containing the lines of text process # # returns - A String with passthrough substitutions performed def apply_passthrough_subs(lines) if (subs = attr('subs', nil, false)) subs = resolve_subs(subs) else subs = SUBS[:pass] end apply_subs lines.join, subs end =end # Internal: Extract the passthrough text from the document for reinsertion after processing. # # text - The String from which to extract passthrough fragements # # returns - The text with the passthrough region substituted with placeholders def extract_passthroughs(text) result = text.dup result.gsub!(REGEXP[:pass_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end if m[1] == '$$' subs = [:specialcharacters] elsif m[3].nil? || m[3].empty? subs = [] else subs = resolve_pass_subs m[3] end # TODO move unescaping closing square bracket to an operation @passthroughs << {:text => m[2] || m[4].gsub('\]', ']'), :subs => subs} index = @passthroughs.size - 1 "\e#{index}\e" } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:')) result.gsub!(REGEXP[:pass_lit]) { # alias match for Ruby 1.8.7 compat m = $~ unescaped_attrs = nil # honor the escape if m[3].start_with? '\\' next m[2].nil? ? "#{m[1]}#{m[3][1..-1]}" : "#{m[1]}[#{m[2]}]#{m[3][1..-1]}" elsif m[1] == '\\' && !m[2].nil? unescaped_attrs = "[#{m[2]}]" end if unescaped_attrs.nil? && !m[2].nil? attributes = parse_attributes(m[2]) else attributes = {} end @passthroughs << {:text => m[4], :subs => [:specialcharacters], :attributes => attributes, :literal => true} index = @passthroughs.size - 1 "#{unescaped_attrs || m[1]}\e#{index}\e" } unless !result.include?('`') result end # Internal: Restore the passthrough text by reinserting into the placeholder positions # # text - The String text into which to restore the passthrough text # # returns The String text with the passthrough text restored def restore_passthroughs(text) return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\e") text.gsub(REGEXP[:pass_placeholder]) { pass = @passthroughs[$1.to_i]; text = apply_subs(pass[:text], pass.fetch(:subs, [])) pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced, :attributes => pass.fetch(:attributes, {})).render : text } end # Public: Substitute special characters (i.e., encode XML) # # Special characters are defined in the Asciidoctor::SPECIAL_CHARS Array constant # # text - The String text to process # # returns The String text with special characters replaced def sub_specialcharacters(text) # this syntax only available in Ruby 1.9 #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] } end alias :sub_specialchars :sub_specialcharacters # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc) # # text - The String text to process # # returns The String text with quoted text rendered using the backend templates def sub_quotes(text) result = text.dup QUOTE_SUBS.each {|type, scope, pattern| result.gsub!(pattern) { transform_quoted_text($~, type, scope) } } result end # Public: Substitute replacement characters (e.g., copyright, trademark, etc) # # text - The String text to process # # returns The String text with the replacement characters substituted def sub_replacements(text) result = text.dup REPLACEMENTS.each {|pattern, replacement, restore| result.gsub!(pattern) { matched = $& head = $1 tail = $2 if matched.include?('\\') matched.tr('\\', '') else case restore when :none replacement when :leading "#{head}#{replacement}" when :bounding "#{head}#{replacement}#{tail}" end end } } result end # Public: Substitute attribute references # # Attribute references are in the format {name}. # # If an attribute referenced in the line is missing, the line is dropped. # # text - The String text to process # # returns The String text with the attribute references replaced with attribute values #-- # NOTE it's necessary to perform this substitution line-by-line # so that a missing key doesn't wipe out the whole block of data def sub_attributes(data, opts = {}) return data if data.nil? || data.empty? string_data = data.is_a? String # normalizes data type to an array (string becomes single-element array) lines = string_data ? [data] : data result = [] lines.each {|line| reject = false line = line.gsub(REGEXP[:attr_ref]) { # alias match for Ruby 1.8.7 compat m = $~ # escaped attribute, return unescaped if !m[1].nil? || !m[4].nil? "{#{m[2]}}" elsif (directive = m[3]) offset = directive.length + 1 expr = m[2][offset..-1] case directive when 'set' args = expr.split(':') _, value = Lexer::store_attribute(args[0], args[1] || '', @document) if value.nil? # since this is an assignment, only drop-line applies here (skip and drop imply the same result) if @document.attributes.fetch('attribute-undefined', COMPLIANCE[:attribute_undefined]) == 'drop-line' Debug.debug { "Undefining attribute: #{key}, line marked for removal" } break '' end end '' when 'counter', 'counter2' args = expr.split(':') val = @document.counter(args[0], args[1]) directive == 'counter2' ? '' : val else # if we get here, our attr_ref regex is too loose warn "asciidoctor: WARNING: illegal attribute directive: #{m[2]}" '' end elsif (key = m[2].downcase) && @document.attributes.has_key?(key) @document.attributes[key] elsif INTRINSICS.has_key? key INTRINSICS[key] else case (opts[:attribute_missing] || @document.attributes.fetch('attribute-missing', COMPLIANCE[:attribute_missing])) when 'skip' "{#{key}}" when 'drop-line' Debug.debug { "Missing attribute: #{key}, line marked for removal" } break '' else # 'drop' '' end end } if line.include? '{' result << line unless reject } string_data ? result.join : result end # Public: Substitute inline macros (e.g., links, images, etc) # # Replace inline macros, which may span multiple lines, in the provided text # # text - The String text to process # # returns The String with the inline macros rendered using the backend templates def sub_macros(text) return text if text.nil? || text.empty? result = text.dup # some look ahead assertions to cut unnecessary regex calls found = {} found[:square_bracket] = result.include?('[') found[:round_bracket] = result.include?('(') found[:colon] = result.include?(':') found[:at] = result.include?('@') found[:macroish] = (found[:square_bracket] && found[:colon]) found[:macroish_short_form] = (found[:square_bracket] && found[:colon] && result.include?(':[')) found[:uri] = (found[:colon] && result.include?('://')) use_link_attrs = @document.attributes.has_key?('linkattrs') experimental = @document.attributes.has_key?('experimental') if experimental if found[:macroish_short_form] && (result.include?('kbd:') || result.include?('btn:')) result.gsub!(REGEXP[:kbd_btn_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end if captured.start_with?('kbd') keys = unescape_bracketed_text m[1] if keys == '+' keys = ['+'] else # need to use closure to work around lack of negative lookbehind keys = keys.split(REGEXP[:kbd_delim]).inject([]) {|c, key| if key.end_with?('++') c << key[0..-3].strip c << '+' else c << key.strip end c } end Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render elsif captured.start_with?('btn') label = unescape_bracketed_text m[1] Inline.new(self, :button, label).render end } end if found[:macroish] && result.include?('menu:') result.gsub!(REGEXP[:menu_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end menu = m[1] items = m[2] if items.nil? submenus = [] menuitem = nil else if (delim = items.include?('>') ? '>' : (items.include?(',') ? ',' : nil)) submenus = items.split(delim).map(&:strip) menuitem = submenus.pop else submenus = [] menuitem = items.rstrip end end Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render } end if result.include?('"') && result.include?('>') result.gsub!(REGEXP[:menu_inline_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if (captured = m[0]).start_with? '\\' next captured[1..-1] end input = m[1] menu, *submenus = input.split('>').map(&:strip) menuitem = submenus.pop Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render } end end # FIXME this location is somewhat arbitrary, probably need to be able to control ordering # TODO this handling needs some cleanup if (extensions = @document.extensions) && extensions.inline_macros? && found[:macroish] extensions.load_inline_macro_processors(@document).each do |processor| result.gsub!(processor.regexp) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end target = m[1] if processor.options[:short_form] attributes = {} else posattrs = processor.options.fetch(:pos_attrs, []) attributes = parse_attributes(m[2], posattrs, :sub_input => true, :unescape_input => true) end processor.process self, target, attributes } end end if found[:macroish] && (result.include?('image:') || result.include?('icon:')) # image:filename.png[Alt Text] result.gsub!(REGEXP[:image_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end raw_attrs = unescape_bracketed_text m[2] if m[0].start_with? 'icon:' type = 'icon' posattrs = ['size'] else type = 'image' posattrs = ['alt', 'width', 'height'] end target = sub_attributes(m[1]) unless type == 'icon' @document.register(:images, target) end attrs = parse_attributes(raw_attrs, posattrs) if !attrs['alt'] attrs['alt'] = File.basename(target, File.extname(target)) end Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).render } end if found[:macroish_short_form] || found[:round_bracket] # indexterm:[Tigers,Big cats] # (((Tigers,Big cats))) result.gsub!(REGEXP[:indexterm_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end terms = unescape_bracketed_text(m[1] || m[2]).split(',').map(&:strip) @document.register(:indexterms, [*terms]) Inline.new(self, :indexterm, text, :attributes => {'terms' => terms}).render } # indexterm2:[Tigers] # ((Tigers)) result.gsub!(REGEXP[:indexterm2_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end text = unescape_bracketed_text(m[1] || m[2]) @document.register(:indexterms, [text]) Inline.new(self, :indexterm, text, :type => :visible).render } end if found[:uri] # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>) result.gsub!(REGEXP[:link_inline]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[2].start_with? '\\' next "#{m[1]}#{m[2][1..-1]}#{m[3]}" # not a valid macro syntax w/o trailing square brackets # we probably shouldn't even get here...our regex is doing too much elsif m[1] == 'link:' && m[3].nil? next m[0] end prefix = (m[1] != 'link:' ? m[1] : '') target = m[2] suffix = '' # strip the <> around the link if prefix.start_with?('<') && target.end_with?('>') prefix = prefix[4..-1] target = target[0..-5] elsif prefix.start_with?('(') && target.end_with?(')') target = target[0..-2] suffix = ')' elsif target.end_with?('):') target = target[0..-3] suffix = '):' end @document.register(:links, target) attrs = nil #text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : '' if !m[3].to_s.empty? if use_link_attrs && (m[3].start_with?('"') || m[3].include?(',')) attrs = parse_attributes(sub_attributes(m[3].gsub('\]', ']')), []) text = attrs[1] else text = sub_attributes(m[3].gsub('\]', ']')) end if text.end_with? '^' text = text.chop attrs ||= {} attrs['window'] = '_blank' unless attrs.has_key?('window') end else text = '' end "#{prefix}#{Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target, :attributes => attrs).render}#{suffix}" } end if found[:macroish] && (result.include?('link:') || result.include?('mailto:')) # inline link macros, link:target[text] result.gsub!(REGEXP[:link_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end raw_target = m[1] mailto = m[0].start_with?('mailto:') target = mailto ? "mailto:#{raw_target}" : raw_target attrs = nil #text = sub_attributes(m[2].gsub('\]', ']')) if use_link_attrs && (m[2].start_with?('"') || m[2].include?(',')) attrs = parse_attributes(sub_attributes(m[2].gsub('\]', ']')), []) text = attrs[1] if mailto if attrs.has_key? 2 target = "#{target}?subject=#{Helpers.encode_uri(attrs[2])}" if attrs.has_key? 3 target = "#{target}&body=#{Helpers.encode_uri(attrs[3])}" end end end else text = sub_attributes(m[2].gsub('\]', ']')) end if text.end_with? '^' text = text.chop attrs ||= {} attrs['window'] = '_blank' unless attrs.has_key?('window') end # QUESTION should a mailto be registered as an e-mail address? @document.register(:links, target) Inline.new(self, :anchor, (!text.empty? ? text : raw_target), :type => :link, :target => target, :attributes => attrs).render } end if found[:at] result.gsub!(REGEXP[:email_inline]) { # alias match for Ruby 1.8.7 compat m = $~ address = m[0] case address[0..0] when '\\' next address[1..-1] when '>', ':' next address end target = "mailto:#{address}" # QUESTION should this be registered as an e-mail address? @document.register(:links, target) Inline.new(self, :anchor, address, :type => :link, :target => target).render } end if found[:macroish_short_form] && result.include?('footnote') result.gsub!(REGEXP[:footnote_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end if m[1] == 'footnote' # hmmmm text = restore_passthroughs(m[2]) id = nil index = @document.counter('footnote-number') @document.register(:footnotes, Document::Footnote.new(index, id, text)) type = nil target = nil else id, text = m[2].split(',', 2).map(&:strip) if !text.nil? # hmmmm text = restore_passthroughs(text) index = @document.counter('footnote-number') @document.register(:footnotes, Document::Footnote.new(index, id, text)) type = :ref target = nil else footnote = @document.references[:footnotes].find {|fn| fn.id == id } target = id id = nil index = footnote.index text = footnote.text type = :xref end end Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render } end if found[:macroish] || result.include?('<<') result.gsub!(REGEXP[:xref_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end if !m[1].nil? id, reftext = m[1].split(',', 2).map(&:strip) id.sub!(REGEXP[:dbl_quoted], '\2') reftext.sub!(REGEXP[:m_dbl_quoted], '\2') unless reftext.nil? else id = m[2] reftext = !m[3].empty? ? m[3] : nil end if id.include? '#' path, fragment = id.split('#') else path = nil fragment = id end # handles form: id if path.nil? refid = fragment target = "##{fragment}" # handles forms: doc#, doc.adoc#, doc#id and doc.adoc#id else path = Helpers.rootname(path) # the referenced path is this document, or its contents has been included in this document if @document.attr?('docname', path) || @document.references[:includes].include?(path) refid = fragment path = nil target = "##{fragment}" else refid = fragment.nil? ? path : "#{path}##{fragment}" path = "#{path}#{@document.attr 'outfilesuffix', '.html'}" target = fragment.nil? ? path : "#{path}##{fragment}" end end Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).render } end if found[:square_bracket] && result.include?('[[[') result.gsub!(REGEXP[:biblio_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end id = reftext = m[1] Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render } end if found[:square_bracket] && result.include?('[[') result.gsub!(REGEXP[:anchor_macro]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end id, reftext = m[1].split(',').map(&:strip) id.sub!(REGEXP[:dbl_quoted], '\2') if reftext.nil? reftext = "[#{id}]" else reftext.sub!(REGEXP[:m_dbl_quoted], '\2') end # NOTE the reftext should also match what's in our references dic if !@document.references[:ids].has_key? id Debug.debug { "Missing reference for anchor #{id}" } end Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render } end result end # Public: Substitute callout references # # text - The String text to process # # returns The String with the callout references rendered using the backend templates def sub_callouts(text) text.gsub(REGEXP[:callout_render]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[1] == '\\' # we have to do a sub since we aren't sure it's the first char next m[0].sub('\\', '') end Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).render } end # Public: Substitute post replacements # # text - The String text to process # # returns The String with the post replacements rendered using the backend templates def sub_post_replacements(text) if @document.attributes['hardbreaks'] lines = text.lines.entries return text if lines.size == 1 last = lines.pop lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render }.push(last) * EOL else text.gsub(REGEXP[:line_break]) { Inline.new(self, :break, $1, :type => :line).render } end end # Internal: Transform (render) a quoted text region # # match - The MatchData for the quoted text region # type - The quoting type (single, double, strong, emphasis, monospaced, etc) # scope - The scope of the quoting (constrained or unconstrained) # # returns The rendered text for the quoted text region def transform_quoted_text(match, type, scope) unescaped_attrs = nil if match[0].start_with? '\\' if scope == :constrained && !match[2].nil? unescaped_attrs = "[#{match[2]}]" else return match[0][1..-1] end end if scope == :constrained if unescaped_attrs.nil? attributes = parse_quoted_text_attributes(match[2]) id = attributes.nil? ? nil : attributes.delete('id') "#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).render}" else "#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type, :attributes => {}).render}" end else attributes = parse_quoted_text_attributes(match[1]) id = attributes.nil? ? nil : attributes.delete('id') Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).render end end # Internal: Parse the attributes that are defined on quoted text # # str - A String of unprocessed attributes (space-separated roles or the id/role shorthand syntax) # # returns nil if str is nil, an empty Hash if str is empty, otherwise a Hash of attributes (role and id only) def parse_quoted_text_attributes(str) return nil if str.nil? return {} if str.empty? str = sub_attributes(str) if str.include?('{') str = str.strip # for compliance, only consider first positional attribute str, _ = str.split(',', 2) if str.include?(',') if str.empty? {} elsif str.start_with?('.') || str.start_with?('#') segments = str.split('#', 2) if segments.length > 1 id, *more_roles = segments[1].split('.') else id = nil more_roles = [] end roles = segments[0].empty? ? [] : segments[0].split('.') if roles.length > 1 roles.shift end if more_roles.length > 0 roles.concat more_roles end attrs = {} attrs['id'] = id unless id.nil? attrs['role'] = roles.empty? ? nil : (roles * ' ') attrs else {'role' => str} end end # Internal: Parse the attributes in the attribute line # # attrline - A String of unprocessed attributes (key/value pairs) # posattrs - The keys for positional attributes # # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes def parse_attributes(attrline, posattrs = ['role'], opts = {}) return nil if attrline.nil? return {} if attrline.empty? attrline = @document.sub_attributes(attrline) if opts[:sub_input] attrline = unescape_bracketed_text(attrline) if opts[:unescape_input] block = nil if opts.fetch(:sub_result, true) # substitutions are only performed on attribute values if block is not nil block = self end if opts.has_key?(:into) AttributeList.new(attrline, block).parse_into(opts[:into], posattrs) else AttributeList.new(attrline, block).parse(posattrs) end end # Internal: Strip bounding whitespace, fold endlines and unescaped closing # square brackets from text extracted from brackets def unescape_bracketed_text(text) return '' if text.empty? text.strip.tr(EOL, ' ').gsub('\]', ']') end # Internal: Resolve the list of comma-delimited subs against the possible options. # # subs - A comma-delimited String of substitution aliases # # returns An Array of Symbols representing the substitution operation def resolve_subs subs, type = :block, subject = nil return [] if subs.nil? || subs.empty? candidates = [] subs.split(',').each do |val| key = val.strip.to_sym # special case to disable callouts for inline subs if key == :verbatim && type == :inline candidates << :specialcharacters elsif COMPOSITE_SUBS.has_key? key candidates.push(*COMPOSITE_SUBS[key]) else candidates << key end end # weed out invalid options and remove duplicates (first wins) resolved = candidates & SUB_OPTIONS[type] if (invalid = candidates - resolved).size > 0 warn "asciidoctor: WARNING: invalid substitution type#{invalid.size > 1 ? 's' : ''}#{subject ? ' for ' : nil}#{subject}: #{invalid * ', '}" end resolved end def resolve_block_subs subs, subject resolve_subs subs, :block, subject end def resolve_pass_subs subs resolve_subs subs, :inline, 'passthrough macro' end # Public: Highlight the source code if a source highlighter is defined # on the document, otherwise return the text unprocessed # # Callout marks are stripped from the source prior to passing it to the # highlighter, then later restored in rendered form, so they are not # incorrectly processed by the source highlighter. # # source - the source code String to highlight # sub_callouts - a Boolean flag indicating whether callout marks should be substituted # # returns the highlighted source code, if a source highlighter is defined # on the document, otherwise the unprocessed text def highlight_source(source, sub_callouts, highlighter = nil) highlighter ||= @document.attributes['source-highlighter'] Helpers.require_library highlighter, (highlighter == 'pygments' ? 'pygments.rb' : highlighter) callout_marks = {} lineno = 0 callout_on_last = false if sub_callouts last = -1 # extract callout marks, indexed by line number source = source.split(EOL).map {|line| lineno = lineno + 1 line.gsub(REGEXP[:callout_scan]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape if m[1] == '\\' m[0].sub('\\', '') else (callout_marks[lineno] ||= []) << m[3] last = lineno nil end } } * EOL callout_on_last = (last == lineno) end linenums_mode = nil case highlighter when 'coderay' result = ::CodeRay::Duo[attr('language', 'text').to_sym, :html, { :css => @document.attributes.fetch('coderay-css', 'class').to_sym, :line_numbers => (linenums_mode = (attr?('linenums') ? @document.attributes.fetch('coderay-linenums-mode', 'table').to_sym : nil)), :line_number_anchors => false}].highlight(source) when 'pygments' lexer = ::Pygments::Lexer[attr('language')] if lexer opts = { :cssclass => 'pyhl', :classprefix => 'tok-', :nobackground => true } opts[:noclasses] = true unless @document.attributes.fetch('pygments-css', 'class') == 'class' if attr? 'linenums' opts[:linenos] = (linenums_mode = @document.attributes.fetch('pygments-linenums-mode', 'table').to_sym).to_s end # FIXME stick these regexs into constants if linenums_mode == :table result = lexer.highlight(source, :options => opts). sub(/
    (.*)<\/div>/m, '\1'). gsub(/]*>(.*?)<\/pre>\s*/m, '\1') else result = lexer.highlight(source, :options => opts). sub(/
    ]*>(.*?)<\/pre><\/div>/m, '\1') end else result = source end end if !sub_callouts || callout_marks.empty? result else lineno = 0 reached_code = linenums_mode != :table result.split(EOL).map {|line| unless reached_code unless line.include?('') next line end reached_code = true end lineno = lineno + 1 if (conums = callout_marks.delete(lineno)) tail = nil if callout_on_last && callout_marks.empty? && (pos = line.index '') tail = line[pos..-1] line = line[0...pos] end if conums.size == 1 %(#{line}#{Inline.new(self, :callout, conums.first, :id => @document.callouts.read_next_id).render }#{tail}) else conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).render } * ' ' %(#{line}#{conums_markup}#{tail}) end else line end } * EOL end end end end asciidoctor-0.1.4/lib/asciidoctor/table.rb000066400000000000000000000407561221220517700205170ustar00rootroot00000000000000module Asciidoctor # Public: Methods and constants for managing AsciiDoc table content in a document. # It supports all three of AsciiDoc's table formats: psv, dsv and csv. class Table < AbstractBlock # Public: A String key that specifies the default table format in AsciiDoc (psv) DEFAULT_DATA_FORMAT = 'psv' # Public: An Array of String keys that represent the table formats in AsciiDoc DATA_FORMATS = ['psv', 'dsv', 'csv'] # Public: A Hash mapping the AsciiDoc table formats to their default delimiters DEFAULT_DELIMITERS = { 'psv' => '|', 'dsv' => ':', 'csv' => ',' } # Public: A Hash mapping styles abbreviations to styles that can be applied # to a table column or cell TEXT_STYLES = { 'd' => :none, 's' => :strong, 'e' => :emphasis, 'm' => :monospaced, 'h' => :header, 'l' => :literal, 'v' => :verse, 'a' => :asciidoc } # Public: A Hash mapping alignment abbreviations to alignments (horizontal # and vertial) that can be applies to a table column or cell ALIGNMENTS = { :h => { '<' => 'left', '>' => 'right', '^' => 'center' }, :v => { '<' => 'top', '>' => 'bottom', '^' => 'middle' } } # Public: Get/Set the columns for this table attr_accessor :columns # Public: Get/Set the Rows struct for this table (encapsulates head, foot # and body rows) attr_accessor :rows # Public: Boolean specifies whether this table has a header row attr_accessor :has_header_option def initialize(parent, attributes) super(parent, :table) @rows = Rows.new([], [], []) @columns = [] @has_header_option = attributes.has_key? 'header-option' # smell like we need a utility method here # to resolve an integer width from potential bogus input pcwidth = attributes['width'] pcwidth_intval = pcwidth.to_i.abs if pcwidth_intval == 0 && pcwidth != "0" || pcwidth_intval > 100 pcwidth_intval = 100 end @attributes['tablepcwidth'] = pcwidth_intval if @document.attributes.has_key? 'pagewidth' @attributes['tableabswidth'] ||= ((@attributes['tablepcwidth'].to_f / 100) * @document.attributes['pagewidth']).round end end # Internal: Returns whether the current row being processed is # the header row def header_row? @has_header_option && @rows.body.size == 0 end # Internal: Creates the Column objects from the column spec # # returns nothing def create_columns(col_specs) total_width = 0 @columns = col_specs.inject([]) {|collector, col_spec| total_width += col_spec['width'] collector << Column.new(self, collector.size, col_spec) collector } if !@columns.empty? @attributes['colcount'] = @columns.size even_width = (100.0 / @columns.size).floor @columns.each {|c| c.assign_width(total_width, even_width) } end nil end # Internal: Partition the rows into header, footer and body as determined # by the options on the table # # returns nothing def partition_header_footer(attributes) # set rowcount before splitting up body rows @attributes['rowcount'] = @rows.body.size if !rows.body.empty? && @has_header_option head = rows.body.shift # styles aren't applied to header row head.each {|c| c.style = nil } # QUESTION why does AsciiDoc use an array for head? is it # possible to have more than one based on the syntax? rows.head = [head] end if !rows.body.empty? && attributes.has_key?('footer-option') rows.foot = [rows.body.pop] end nil end end # Public: A struct that encapsulates the collection of rows (head, foot, body) for a table Table::Rows = Struct.new(:head, :foot, :body) # Public: Methods to manage the columns of an AsciiDoc table. In particular, it # keeps track of the column specs class Table::Column < AbstractNode # Public: Get/Set the Symbol style for this column. attr_accessor :style def initialize(table, index, attributes = {}) super(table, :column) @style = attributes['style'] attributes['colnumber'] = index + 1 attributes['width'] ||= 1 attributes['halign'] ||= 'left' attributes['valign'] ||= 'top' update_attributes(attributes) end # Public: An alias to the parent block (which is always a Table) alias :table :parent # Internal: Calculate and assign the widths (percentage and absolute) for this column # # This method assigns the colpcwidth and colabswidth attributes. # # returns nothing def assign_width(total_width, even_width) if total_width > 0 width = ((@attributes['width'].to_f / total_width) * 100).floor else width = even_width end @attributes['colpcwidth'] = width if parent.attributes.has_key? 'tableabswidth' @attributes['colabswidth'] = ((width.to_f / 100) * parent.attributes['tableabswidth']).round end nil end end # Public: Methods for managing the a cell in an AsciiDoc table. class Table::Cell < AbstractNode # Public: Get/Set the Symbol style for this cell (default: nil) attr_accessor :style # Public: An Integer of the number of columns this cell will span (default: nil) attr_accessor :colspan # Public: An Integer of the number of rows this cell will span (default: nil) attr_accessor :rowspan # Public: An alias to the parent block (which is always a Column) alias :column :parent # Public: The internal Asciidoctor::Document for a cell that has the asciidoc style attr_reader :inner_document def initialize(column, text, attributes = {}, cursor = nil) super(column, :cell) @text = text @style = nil @colspan = nil @rowspan = nil # TODO feels hacky if !column.nil? @style = column.attributes['style'] update_attributes(column.attributes) end if !attributes.nil? @colspan = attributes.delete('colspan') @rowspan = attributes.delete('rowspan') # TODO eventualy remove the style attribute from the attributes hash #@style = attributes.delete('style') if attributes.has_key? 'style' @style = attributes['style'] if attributes.has_key? 'style' update_attributes(attributes) end # only allow AsciiDoc cells in non-header rows if @style == :asciidoc && !column.table.header_row? # FIXME hide doctitle from nested document; temporary workaround to fix # nested document seeing doctitle and assuming it has its own document title parent_doctitle = @document.attributes.delete('doctitle') # NOTE we need to process the first line of content as it may not have been processed # the included content cannot expect to match conditional terminators in the remaining # lines of table cell content, it must be self-contained logic inner_document_lines = @text.each_line.to_a unless inner_document_lines.empty? || !inner_document_lines.first.include?('::') unprocessed_lines = inner_document_lines[0..0] processed_lines = PreprocessorReader.new(@document, unprocessed_lines).readlines if processed_lines != unprocessed_lines inner_document_lines.shift inner_document_lines.unshift(*processed_lines) end end @inner_document = Document.new(inner_document_lines, :header_footer => false, :parent => @document, :cursor => cursor) @document.attributes['doctitle'] = parent_doctitle unless parent_doctitle.nil? end end # Public: Get the text with normal substitutions applied for this cell. Used for cells in the head rows def text apply_normal_subs(@text).strip end # Public: Handles the body data (tbody, tfoot), applying styles and partitioning into paragraphs def content if @style == :asciidoc @inner_document.render else text.split(BLANK_LINE_PATTERN).map {|p| !@style || @style == :header ? p : Inline.new(parent, :quoted, p, :type => @style).render } end end def to_s "#{super.to_s} - [text: #@text, colspan: #{@colspan || 1}, rowspan: #{@rowspan || 1}, attributes: #@attributes]" end end # Public: Methods for managing the parsing of an AsciiDoc table. Instances of this # class are primarily responsible for tracking the buffer of a cell as the parser # moves through the lines of the table using tail recursion. When a cell boundary # is located, the previous cell is closed, an instance of Table::Cell is # instantiated, the row is closed if the cell satisifies the column count and, # finally, a new buffer is allocated to track the next cell. class Table::ParserContext # Public: The Table currently being parsed attr_accessor :table # Public: The AsciiDoc table format (psv, dsv or csv) attr_accessor :format # Public: Get the expected column count for a row # # col_count is the number of columns to pull into a row # A value of -1 means we use the number of columns found # in the first line as the col_count attr_reader :col_count # Public: The String buffer of the currently open cell attr_accessor :buffer # Public: The cell delimiter for this table. attr_reader :delimiter # Public: The cell delimiter compiled Regexp for this table. attr_reader :delimiter_re def initialize(reader, table, attributes = {}) @reader = reader @table = table # TODO if reader.cursor becomes a reference, this would require .dup @last_cursor = reader.cursor if attributes.has_key? 'format' @format = attributes['format'] if !Table::DATA_FORMATS.include? @format raise "Illegal table format: #@format" end else @format = Table::DEFAULT_DATA_FORMAT end if @format == 'psv' && !attributes.has_key?('separator') && table.document.nested? @delimiter = '!' else @delimiter = attributes.fetch('separator', Table::DEFAULT_DELIMITERS[@format]) end @delimiter_re = /#{Regexp.escape @delimiter}/ @col_count = table.columns.empty? ? -1 : table.columns.size @buffer = '' @cell_specs = [] @cell_open = false @active_rowspans = [0] @col_visits = 0 @current_row = [] @linenum = -1 end # Public: Checks whether the line provided starts with the cell delimiter # used by this table. # # returns true if the line starts with the delimiter, false otherwise def starts_with_delimiter?(line) line.start_with? @delimiter end # Public: Checks whether the line provided contains the cell delimiter # used by this table. # # returns MatchData if the line contains the delimiter, false otherwise def match_delimiter(line) line.match @delimiter_re end # Public: Skip beyond the matched delimiter because it was a false positive # (either because it was escaped or in a quoted context) # # returns the String after the match def skip_matched_delimiter(match, escaped = false) @buffer = %(#@buffer#{escaped ? match.pre_match.chop : match.pre_match}#@delimiter) match.post_match end # Public: Determines whether the buffer has unclosed quotes. Used for CSV data. # # returns true if the buffer has unclosed quotes, false if it doesn't or it # isn't quoted data def buffer_has_unclosed_quotes?(append = nil) record = "#@buffer#{append}".strip record.start_with?('"') && !record.start_with?('""') && !record.end_with?('"') end # Public: Determines whether the buffer contains quoted data. Used for CSV data. # # returns true if the buffer starts with a double quote (and not an escaped double quote), # false otherwise def buffer_quoted? @buffer.lstrip! @buffer.start_with?('"') && !@buffer.start_with?('""') end # Public: Takes a cell spec from the stack. Cell specs precede the delimiter, so a # stack is used to carry over the spec from the previous cell to the current cell # when the cell is being closed. # # returns The cell spec Hash captured from parsing the previous cell def take_cell_spec() @cell_specs.shift end # Public: Puts a cell spec onto the stack. Cell specs precede the delimiter, so a # stack is used to carry over the spec to the next cell. # # returns nothing def push_cell_spec(cell_spec = {}) # this shouldn't be nil, but we check anyway @cell_specs << (cell_spec || {}) nil end # Public: Marks that the cell should be kept open. Used when the end of the line is # reached and the cell may contain additional text. # # returns nothing def keep_cell_open @cell_open = true nil end # Public: Marks the cell as closed so that the parser knows to instantiate a new cell # instance and add it to the current row. # # returns nothing def mark_cell_closed @cell_open = false nil end # Public: Checks whether the current cell is still open # # returns true if the cell is marked as open, false otherwise def cell_open? @cell_open end # Public: Checks whether the current cell has been marked as closed # # returns true if the cell is marked as closed, false otherwise def cell_closed? !@cell_open end # Public: If the current cell is open, close it. In additional, push the # cell spec captured from the end of this cell onto the stack for use # by the next cell. # # returns nothing def close_open_cell(next_cell_spec = {}) push_cell_spec next_cell_spec close_cell(true) if cell_open? advance nil end # Public: Close the current cell, instantiate a new Table::Cell, add it to # the current row and, if the number of expected columns for the current # row has been met, close the row and begin a new one. # # returns nothing def close_cell(eol = false) cell_text = @buffer.strip @buffer = '' if format == 'psv' cell_spec = take_cell_spec if cell_spec.nil? warn "asciidoctor: ERROR: #{@last_cursor.line_info}: table missing leading separator, recovering automatically" cell_spec = {} repeat = 1 else repeat = cell_spec.fetch('repeatcol', 1) cell_spec.delete('repeatcol') end else cell_spec = nil repeat = 1 if format == 'csv' if !cell_text.empty? && cell_text.include?('"') # this may not be perfect logic, but it hits the 99% if cell_text.start_with?('"') && cell_text.end_with?('"') # unquote cell_text = cell_text[1..-2].strip end # collapses escaped quotes cell_text = cell_text.tr_s('"', '"') end end end 1.upto(repeat) {|i| # make column resolving an operation if @col_count == -1 @table.columns << Table::Column.new(@table, @current_row.size + i - 1) column = @table.columns.last else # QUESTION is this right for cells that span columns? column = @table.columns[@current_row.size] end cell = Table::Cell.new(column, cell_text, cell_spec, @last_cursor) @last_cursor = @reader.cursor unless cell.rowspan.nil? || cell.rowspan == 1 activate_rowspan(cell.rowspan, (cell.colspan || 1)) end @col_visits += (cell.colspan || 1) @current_row << cell # don't close the row if we're on the first line and the column count has not been set explicitly # TODO perhaps the col_count/linenum logic should be in end_of_row? (or a should_end_row? method) close_row if end_of_row? && (@col_count != -1 || @linenum > 0 || (eol && i == repeat)) } @open_cell = false nil end # Public: Close the row by adding it to the Table and resetting the row # Array and counter variables. # # returns nothing def close_row @table.rows.body << @current_row # don't have to account for active rowspans here # since we know this is first row @col_count = @col_visits if @col_count == -1 @col_visits = 0 @current_row = [] @active_rowspans.shift @active_rowspans[0] ||= 0 nil end # Public: Activate a rowspan. The rowspan Array is consulted when # determining the effective number of cells in the current row. # # returns nothing def activate_rowspan(rowspan, colspan) 1.upto(rowspan - 1).each {|i| @active_rowspans[i] ||= 0 @active_rowspans[i] += colspan } nil end # Public: Check whether we've met the number of effective columns for the current row. def end_of_row? @col_count == -1 || effective_col_visits == @col_count end # Public: Calculate the effective column visits, which consists of the number of # cells plus any active rowspans. def effective_col_visits @col_visits + @active_rowspans.first end # Internal: Advance to the next line (which may come after the parser begins processing # the next line if the last cell had wrapped content). def advance @linenum += 1 end end end asciidoctor-0.1.4/lib/asciidoctor/version.rb000066400000000000000000000000531221220517700210770ustar00rootroot00000000000000module Asciidoctor VERSION = '0.1.4' end asciidoctor-0.1.4/man/000077500000000000000000000000001221220517700145715ustar00rootroot00000000000000asciidoctor-0.1.4/man/asciidoctor.1000066400000000000000000000154401221220517700171620ustar00rootroot00000000000000'\" t .\" Title: asciidoctor .\" Author: Dan Allen .\" Generator: Asciidoctor 0.1.4 .\" Date: 2013-09-05 .\" Manual: Asciidoctor Manual .\" Source: Asciidoctor 0.1.4 .\" Language: English .\" .TH "ASCIIDOCTOR" "1" "2013-09-05" "Asciidoctor 0\&.1\&.4" "Asciidoctor Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .nh .ad l .SH "SYNOPSIS" .sp \fBasciidoctor\fR [\fIOPTION\fR]\&... \fIFILE\fR\&... .SH "DESCRIPTION" .sp The asciidoctor(1) command converts the AsciiDoc source file(s) \fIFILE\fR to HTML 5, DocBook 4\&.5, DocBook 5\&.0 and other custom output formats\&. .sp If \fIFILE\fR is \fI\-\fR then the AsciiDoc source is read from standard input\&. .SH "OPTIONS" .SS "Security Settings" .PP \fB\-B, \-\-base\-dir\fR=\fIDIR\fR .RS 4 Base directory containing the document and resources\&. Defaults to the directory containing the source file, or the working directory if the source is read from a stream\&. Can be used as a way to chroot the execution of the program\&. .RE .PP \fB\-S, \-\-safe\-mode\fR=\fISAFE_MODE\fR .RS 4 Set safe mode level: \fIunsafe\fR, \fIsafe\fR, \fIserver\fR or \fIsecure\fR\&. Disables potentially dangerous macros in source files, such as include::[]\&. If not set, the safe mode level defaults to \fIunsafe\fR when Asciidoctor is invoked using this script\&. .RE .PP \fB\-\-safe\fR .RS 4 Set safe mode level to \fIsafe\fR\&. Enables include macros, but restricts access to ancestor paths of source file\&. Provided for compatibility with the asciidoc command\&. If not set, the safe mode level defaults to \fIunsafe\fR when Asciidoctor is invoked using this script\&. .RE .SS "Document Settings" .PP \fB\-a, \-\-attribute\fR=\fIATTRIBUTE\fR .RS 4 Define, override or delete a document attribute\&. Command\-line attributes take precedence over attributes defined in the source file\&. .sp \fIATTRIBUTE\fR is normally formatted as a key\-value pair, in the form \fINAME=VALUE\fR\&. Alternate acceptable forms are \fINAME\fR (where the \fIVALUE\fR defaults to an empty string), \fINAME!\fR (unassigns the \fINAME\fR attribute) and \fINAME=VALUE@\fR (where \fIVALUE\fR does not override value of \fINAME\fR attribute if it\(cqs already defined in the source document)\&. Values containing spaces should be enclosed in quotes\&. .sp This option may be specified more than once\&. .RE .PP \fB\-b, \-\-backend\fR=\fIBACKEND\fR .RS 4 Backend output file format: \fIhtml5\fR, \fIdocbook45\fR and \fIdocbook5\fR supported out of the box\&. You can also use the backend alias names \fIhtml\fR (aliased to \fIhtml5\fR) or \fIdocbook\fR (aliased to \fIdocbook45\fR)\&. Defaults to \fIhtml5\fR\&. Other options can be passed, but if Asciidoctor cannot find the backend, it will fail during rendering\&. .RE .PP \fB\-d, \-\-doctype\fR=\fIDOCTYPE\fR .RS 4 Document type: \fIarticle\fR, \fIbook\fR, \fImanpage\fR or \fIinline\fR\&. Sets the root element when using the \fIdocbook\fR backend and the style class on the HTML body element when using the \fIhtml\fR backend\&. The \fIbook\fR document type allows multiple level\-0 section titles in a single document\&. The \fImanpage\fR document type enables parsing of metadata necessary to produce a manpage\&. The \fIinline\fR document type allows the content of a single paragraph to be formatted and returned without wrapping it in a containing element\&. Defaults to \fIarticle\fR\&. .RE .SS "Rendering Control" .PP \fB\-C, \-\-compact\fR .RS 4 Compact the output by removing blank lines\&. Not enabled by default\&. .RE .PP \fB\-D, \-\-destination\-dir\fR=\fIDIR\fR .RS 4 Destination output directory\&. Defaults to the directory containing the source file, or the working directory if the source is read from a stream\&. If specified, the directory is resolved relative to the working directory\&. .RE .PP \fB\-E, \-\-template\-engine\fR=\fINAME\fR .RS 4 Template engine to use for the custom render templates\&. The gem with the same name as the engine will be loaded automatically\&. This name is also used to build the full path to the custom templates\&. .RE .PP \fB\-e, \-\-eruby\fR .RS 4 Specifies the eRuby implementation to use for rendering the built\-in templates\&. Supported values are \fIerb\fR and \fIerubis\fR\&. Defaults to \fIerb\fR\&. .RE .PP \fB\-n, \-\-section\-numbers\fR .RS 4 Auto\-number section titles\&. Synonym for \fB\-\-attribute numbered\fR\&. .RE .PP \fB\-o, \-\-out\-file\fR=\fIOUT_FILE\fR .RS 4 Write output to file \fIOUT_FILE\fR\&. Defaults to the base name of the input file suffixed with \fIbackend\fR extension\&. If the input is read from standard input, then the output file defaults to stdout\&. If \fIOUT_FILE\fR is \fI\-\fR then the standard output is also used\&. If specified, the file is resolved relative to the working directory\&. .RE .PP \fB\-s, \-\-no\-header\-footer\fR .RS 4 Suppress the document header and footer in the output\&. .RE .PP \fB\-T, \-\-template\-dir\fR=\fIDIR\fR .RS 4 A directory containing custom render templates that override one or more templates from the built\-in set\&. (requires \fItilt\fR gem) .sp If there is a subfolder that matches the engine name (if specified), that folder is appended to the template directory path\&. Similarly, if there is a subfolder in the resulting template directory that matches the name of the backend, that folder is appended to the template directory path\&. .sp This option may be specified more than once\&. Matching templates found in subsequent directories override ones previously discovered\&. .RE .SS "Processing Information" .PP \fB\-\-trace\fR .RS 4 Include backtrace information on errors\&. Not enabled by default\&. .RE .PP \fB\-v, \-\-verbose\fR .RS 4 Verbosely print processing information and configuration file checks to stderr\&. .RE .SS "Program Information" .PP \fB\-h, \-\-help\fR .RS 4 Show the help message\&. .RE .PP \fB\-V, \-\-version\fR .RS 4 Print program version number\&. .RE .SH "EXIT STATUS" .PP \fB0\fR .RS 4 Success .RE .PP \fB1\fR .RS 4 Failure (syntax or usage error; configuration error; document processing failure; unexpected error)\&. .RE .SH "BUGS" .sp See the \fBAsciidoctor\fR issue tracker: <\fBhttps://github\&.com/asciidoctor/asciidoctor/issues?state=open\fR> .SH "AUTHORS" .sp \fBAsciidoctor\fR was written by Dan Allen, Ryan Waldron, Jason Porter, Nick Hengeveld and other contributors\&. .sp \fBAsciiDoc\fR was written by Stuart Rackham and has received contributions from many other individuals\&. .SH "RESOURCES" .sp Git source repository on GitHub: <\fBhttps://github\&.com/asciidoctor/asciidoctor\fR> .sp Project web site: <\fBhttp://asciidoctor\&.org\fR> .sp GitHub organization: <\fBhttp://github\&.com/asciidoctor\fR> .sp Mailinglist / forum: <\fBhttp://discuss\&.asciidoctor\&.org\fR> .SH "COPYING" .sp Copyright (C) 2012\-2013 Dan Allen and Ryan Waldron\&. Free use of this software is granted under the terms of the MIT License\&. .SH "AUTHOR" .PP \fBDan Allen\fR .RS 4 Author. .RE asciidoctor-0.1.4/man/asciidoctor.adoc000066400000000000000000000140551221220517700177310ustar00rootroot00000000000000= asciidoctor(1) Dan Allen; Ryan Waldron :doctype: manpage :man manual: Asciidoctor Manual :man source: Asciidoctor 0.1.4 :awestruct-layout: base == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats == SYNOPSIS *asciidoctor* ['OPTION']... 'FILE'... == DESCRIPTION The asciidoctor(1) command converts the AsciiDoc source file(s) 'FILE' to HTML 5, DocBook 4.5, DocBook 5.0 and other custom output formats. If 'FILE' is '-' then the AsciiDoc source is read from standard input. == OPTIONS === Security Settings *-B, --base-dir*='DIR':: Base directory containing the document and resources. Defaults to the directory containing the source file, or the working directory if the source is read from a stream. Can be used as a way to chroot the execution of the program. *-S, --safe-mode*='SAFE_MODE':: Set safe mode level: 'unsafe', 'safe', 'server' or 'secure'. Disables potentially dangerous macros in source files, such as include::[]. If not set, the safe mode level defaults to 'unsafe' when Asciidoctor is invoked using this script. *--safe*:: Set safe mode level to 'safe'. Enables include macros, but restricts access to ancestor paths of source file. Provided for compatibility with the asciidoc command. If not set, the safe mode level defaults to 'unsafe' when Asciidoctor is invoked using this script. === Document Settings *-a, --attribute*='ATTRIBUTE':: Define, override or delete a document attribute. Command-line attributes take precedence over attributes defined in the source file. + 'ATTRIBUTE' is normally formatted as a key-value pair, in the form 'NAME=VALUE'. Alternate acceptable forms are 'NAME' (where the 'VALUE' defaults to an empty string), 'NAME!' (unassigns the 'NAME' attribute) and 'NAME=VALUE@' (where 'VALUE' does not override value of 'NAME' attribute if it's already defined in the source document). Values containing spaces should be enclosed in quotes. + This option may be specified more than once. *-b, --backend*='BACKEND':: Backend output file format: 'html5', 'docbook45' and 'docbook5' supported out of the box. You can also use the backend alias names 'html' (aliased to 'html5') or 'docbook' (aliased to 'docbook45'). Defaults to 'html5'. Other options can be passed, but if Asciidoctor cannot find the backend, it will fail during rendering. *-d, --doctype*='DOCTYPE':: Document type: 'article', 'book', 'manpage' or 'inline'. Sets the root element when using the 'docbook' backend and the style class on the HTML body element when using the 'html' backend. The 'book' document type allows multiple level-0 section titles in a single document. The 'manpage' document type enables parsing of metadata necessary to produce a manpage. The 'inline' document type allows the content of a single paragraph to be formatted and returned without wrapping it in a containing element. Defaults to 'article'. === Rendering Control *-C, --compact*:: Compact the output by removing blank lines. Not enabled by default. *-D, --destination-dir*='DIR':: Destination output directory. Defaults to the directory containing the source file, or the working directory if the source is read from a stream. If specified, the directory is resolved relative to the working directory. *-E, --template-engine*='NAME':: Template engine to use for the custom render templates. The gem with the same name as the engine will be loaded automatically. This name is also used to build the full path to the custom templates. *-e, --eruby*:: Specifies the eRuby implementation to use for rendering the built-in templates. Supported values are 'erb' and 'erubis'. Defaults to 'erb'. *-n, --section-numbers*:: Auto-number section titles. Synonym for *--attribute numbered*. *-o, --out-file*='OUT_FILE':: Write output to file 'OUT_FILE'. Defaults to the base name of the input file suffixed with 'backend' extension. If the input is read from standard input, then the output file defaults to stdout. If 'OUT_FILE' is '-' then the standard output is also used. If specified, the file is resolved relative to the working directory. *-s, --no-header-footer*:: Suppress the document header and footer in the output. *-T, --template-dir*='DIR':: A directory containing custom render templates that override one or more templates from the built-in set. (requires 'tilt' gem) + If there is a subfolder that matches the engine name (if specified), that folder is appended to the template directory path. Similarly, if there is a subfolder in the resulting template directory that matches the name of the backend, that folder is appended to the template directory path. + This option may be specified more than once. Matching templates found in subsequent directories override ones previously discovered. === Processing Information *--trace*:: Include backtrace information on errors. Not enabled by default. *-v, --verbose*:: Verbosely print processing information and configuration file checks to stderr. === Program Information *-h, --help*:: Show the help message. *-V, --version*:: Print program version number. == EXIT STATUS *0*:: Success *1*:: Failure (syntax or usage error; configuration error; document processing failure; unexpected error). == BUGS See the *Asciidoctor* issue tracker: <**https://github.com/asciidoctor/asciidoctor/issues?state=open**> == AUTHORS *Asciidoctor* was written by Dan Allen, Ryan Waldron, Jason Porter, Nick Hengeveld and other contributors. *AsciiDoc* was written by Stuart Rackham and has received contributions from many other individuals. == RESOURCES Git source repository on GitHub: <**https://github.com/asciidoctor/asciidoctor**> Project web site: <**http://asciidoctor.org**> GitHub organization: <**http://github.com/asciidoctor**> Mailinglist / forum: <**http://discuss.asciidoctor.org**> == COPYING Copyright \(C) 2012-2013 Dan Allen and Ryan Waldron. Free use of this software is granted under the terms of the MIT License. // vim: tw=80 asciidoctor-0.1.4/test/000077500000000000000000000000001221220517700147755ustar00rootroot00000000000000asciidoctor-0.1.4/test/attributes_test.rb000066400000000000000000000605151221220517700205560ustar00rootroot00000000000000require 'test_helper' context 'Attributes' do context 'Assignment' do test 'creates an attribute' do doc = document_from_string(':frog: Tanglefoot') assert_equal 'Tanglefoot', doc.attributes['frog'] end test 'requires a space after colon following attribute name' do doc = document_from_string 'foo:bar' assert_equal nil, doc.attributes['foo'] end test 'creates an attribute by fusing a multi-line value' do str = <<-EOS :description: This is the first + Ruby implementation of + AsciiDoc. EOS doc = document_from_string(str) assert_equal 'This is the first Ruby implementation of AsciiDoc.', doc.attributes['description'] end test 'should delete an attribute that ends with !' do doc = document_from_string(":frog: Tanglefoot\n:frog!:") assert_equal nil, doc.attributes['frog'] end test 'should delete an attribute that ends with ! set via API' do doc = document_from_string(":frog: Tanglefoot", :attributes => {'frog!' => ''}) assert_equal nil, doc.attributes['frog'] end test 'should delete an attribute that begins with !' do doc = document_from_string(":frog: Tanglefoot\n:!frog:") assert_equal nil, doc.attributes['frog'] end test 'should delete an attribute that begins with ! set via API' do doc = document_from_string(":frog: Tanglefoot", :attributes => {'!frog' => ''}) assert_equal nil, doc.attributes['frog'] end test 'should delete an attribute set via API to nil value' do doc = document_from_string(":frog: Tanglefoot", :attributes => {'frog' => nil}) assert_equal nil, doc.attributes['frog'] end test "doesn't choke when deleting a non-existing attribute" do doc = document_from_string(':frog!:') assert_equal nil, doc.attributes['frog'] end test "replaces special characters in attribute value" do doc = document_from_string(":xml-busters: <>&") assert_equal '<>&', doc.attributes['xml-busters'] end test "performs attribute substitution on attribute value" do doc = document_from_string(":version: 1.0\n:release: Asciidoctor {version}") assert_equal 'Asciidoctor 1.0', doc.attributes['release'] end test "assigns attribute to empty string if substitution fails to resolve attribute" do doc = document_from_string ":release: Asciidoctor {version}", :attributes => { 'attribute-missing' => 'drop-line' } assert_equal '', doc.attributes['release'] end test "assigns multi-line attribute to empty string if substitution fails to resolve attribute" do doc = document_from_string ":release: Asciidoctor +\n {version}", :attributes => { 'attribute-missing' => 'drop-line' } assert_equal '', doc.attributes['release'] end test "apply custom substitutions to text in passthrough macro and assign to attribute" do doc = document_from_string(":xml-busters: pass:[<>&]") assert_equal '<>&', doc.attributes['xml-busters'] doc = document_from_string(":xml-busters: pass:none[<>&]") assert_equal '<>&', doc.attributes['xml-busters'] doc = document_from_string(":xml-busters: pass:specialcharacters[<>&]") assert_equal '<>&', doc.attributes['xml-busters'] end test "attribute is treated as defined until it's not" do input = <<-EOS :holygrail: ifdef::holygrail[] The holy grail has been found! endif::holygrail[] :holygrail!: ifndef::holygrail[] Buggers! What happened to the grail? endif::holygrail[] EOS output = render_string input assert_xpath '//p', output, 2 assert_xpath '(//p)[1][text() = "The holy grail has been found!"]', output, 1 assert_xpath '(//p)[2][text() = "Buggers! What happened to the grail?"]', output, 1 end # Validates requirement: "Header attributes are overridden by command-line attributes." test 'attribute defined in document options overrides attribute in document' do doc = document_from_string(':cash: money', :attributes => {'cash' => 'heroes'}) assert_equal 'heroes', doc.attributes['cash'] end test 'attribute defined in document options cannot be unassigned in document' do doc = document_from_string(':cash!:', :attributes => {'cash' => 'heroes'}) assert_equal 'heroes', doc.attributes['cash'] end test 'attribute undefined in document options cannot be assigned in document' do doc = document_from_string(':cash: money', :attributes => {'cash!' => '' }) assert_equal nil, doc.attributes['cash'] doc = document_from_string(':cash: money', :attributes => {'cash' => nil }) assert_equal nil, doc.attributes['cash'] end test 'backend attributes are updated if backend attribute is defined in document and safe mode is less than SERVER' do doc = document_from_string(':backend: docbook45', :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'docbook45', doc.attributes['backend'] assert doc.attributes.has_key? 'backend-docbook45' assert_equal 'docbook', doc.attributes['basebackend'] assert doc.attributes.has_key? 'basebackend-docbook' end test 'backend attributes defined in document options overrides backend attribute in document' do doc = document_from_string(':backend: docbook45', :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'backend' => 'html5'}) assert_equal 'html5', doc.attributes['backend'] assert doc.attributes.has_key? 'backend-html5' assert_equal 'html', doc.attributes['basebackend'] assert doc.attributes.has_key? 'basebackend-html' end end context 'Interpolation' do test "render properly with simple names" do html = render_string(":frog: Tanglefoot\n:my_super-hero: Spiderman\n\nYo, {frog}!\nBeat {my_super-hero}!") result = Nokogiri::HTML(html) assert_equal "Yo, Tanglefoot!\nBeat Spiderman!", result.css("p").first.content.strip end test 'attribute lookup is not case sensitive' do input = <<-EOS :He-Man: The most powerful man in the universe He-Man: {He-Man} She-Ra: {She-Ra} EOS result = render_embedded_string input, :attributes => {'She-Ra' => 'The Princess of Power'} assert_xpath '//p[text()="He-Man: The most powerful man in the universe"]', result, 1 assert_xpath '//p[text()="She-Ra: The Princess of Power"]', result, 1 end test "render properly with single character name" do html = render_string(":r: Ruby\n\nR is for {r}!") result = Nokogiri::HTML(html) assert_equal 'R is for Ruby!', result.css("p").first.content.strip end test "collapses spaces in attribute names" do input = <<-EOS Main Header =========== :My frog: Tanglefoot Yo, {myfrog}! EOS output = render_string input assert_xpath '(//p)[1][text()="Yo, Tanglefoot!"]', output, 1 end test "ignores lines with bad attributes if attribute-missing is drop-line" do input = <<-EOS :attribute-missing: drop-line This is blah blah {foobarbaz} all there is. EOS html = render_embedded_string input result = Nokogiri::HTML(html) assert_no_match(/blah blah/m, result.css("p").first.content.strip) end test "attribute value gets interpretted when rendering" do doc = document_from_string(":google: http://google.com[Google]\n\n{google}") assert_equal 'http://google.com[Google]', doc.attributes['google'] output = doc.render assert_xpath '//a[@href="http://google.com"][text() = "Google"]', output, 1 end test 'should drop line with reference to missing attribute if attribute-missing attribute is drop-line' do input = <<-EOS :attribute-missing: drop-line Line 1: This line should appear in the output. Line 2: Oh no, a {bogus-attribute}! This line should not appear in the output. EOS output = render_embedded_string input assert_match(/Line 1/, output) assert_no_match(/Line 2/, output) end test 'should not drop line with reference to missing attribute by default' do input = <<-EOS Line 1: This line should appear in the output. Line 2: A {bogus-attribute}! This time, this line should appear in the output. EOS output = render_embedded_string input assert_match(/Line 1/, output) assert_match(/Line 2/, output) assert_match(/\{bogus-attribute\}/, output) end test 'should drop line with attribute unassignment by default' do input = <<-EOS :a: Line 1: This line should appear in the output. Line 2: {set:a!}This line should not appear in the output. EOS output = render_embedded_string input assert_match(/Line 1/, output) assert_no_match(/Line 2/, output) end test 'should not drop line with attribute unassignment if attribute-undefined is drop' do input = <<-EOS :attribute-undefined: drop :a: Line 1: This line should appear in the output. Line 2: {set:a!}This line should not appear in the output. EOS output = render_embedded_string input assert_match(/Line 1/, output) assert_match(/Line 2/, output) assert_no_match(/\{set:a!\}/, output) end test "substitutes inside unordered list items" do html = render_string(":foo: bar\n* snort at the {foo}\n* yawn") result = Nokogiri::HTML(html) assert_match(/snort at the bar/, result.css("li").first.content.strip) end test 'substitutes inside section title' do output = render_string(":prefix: Cool\n\n== {prefix} Title\n\ncontent") result = Nokogiri::HTML(output) assert_match(/Cool Title/, result.css('h2').first.content) assert_match(/_cool_title/, result.css('h2').first.attr('id')) end test 'interpolates attribute defined in header inside attribute entry in header' do input = <<-EOS = Title Author Name :attribute-a: value :attribute-b: {attribute-a} preamble EOS doc = document_from_string(input, :parse_header_only => true) assert_equal 'value', doc.attributes['attribute-b'] end test 'interpolates author attribute inside attribute entry in header' do input = <<-EOS = Title Author Name :name: {author} preamble EOS doc = document_from_string(input, :parse_header_only => true) assert_equal 'Author Name', doc.attributes['name'] end test 'interpolates revinfo attribute inside attribute entry in header' do input = <<-EOS = Title Author Name 2013-01-01 :date: {revdate} preamble EOS doc = document_from_string(input, :parse_header_only => true) assert_equal '2013-01-01', doc.attributes['date'] end test 'attribute entries can resolve previously defined attributes' do input = <<-EOS = Title Author Name v1.0, 2010-01-01: First release! :a: value :a2: {a} :revdate2: {revdate} {a} == {a2} {revdate} == {revdate2} EOS doc = document_from_string input assert_equal '2010-01-01', doc.attr('revdate') assert_equal '2010-01-01', doc.attr('revdate2') assert_equal 'value', doc.attr('a') assert_equal 'value', doc.attr('a2') output = doc.render assert output.include?('value == value') assert output.include?('2010-01-01 == 2010-01-01') end test 'substitutes inside block title' do input = <<-EOS :gem_name: asciidoctor .Require the +{gem_name}+ gem To use {gem_name}, the first thing to do is to import it in your Ruby source file. EOS output = render_embedded_string input assert_xpath '//*[@class="title"]/code[text()="asciidoctor"]', output, 1 end test 'renders attribute until it is deleted' do input = <<-EOS :foo: bar Crossing the {foo}. :foo!: Belly up to the {foo}. EOS output = render_embedded_string input assert_xpath '//p[text()="Crossing the bar."]', output, 1 assert_xpath '//p[text()="Belly up to the bar."]', output, 0 end test 'does not disturb attribute-looking things escaped with backslash' do html = render_string(":foo: bar\nThis is a \\{foo} day.") result = Nokogiri::HTML(html) assert_equal 'This is a {foo} day.', result.css('p').first.content.strip end test 'does not disturb attribute-looking things escaped with literals' do html = render_string(":foo: bar\nThis is a +++{foo}+++ day.") result = Nokogiri::HTML(html) assert_equal 'This is a {foo} day.', result.css('p').first.content.strip end test 'does not substitute attributes inside listing blocks' do input = <<-EOS :forecast: snow ---- puts 'The forecast for today is {forecast}' ---- EOS output = render_string(input) assert_match(/\{forecast\}/, output) end test 'does not substitute attributes inside literal blocks' do input = <<-EOS :foo: bar .... You insert the text {foo} to expand the value of the attribute named foo in your document. .... EOS output = render_string(input) assert_match(/\{foo\}/, output) end test 'does not show docdir and shows relative docfile if safe mode is SERVER or greater' do input = <<-EOS * docdir: {docdir} * docfile: {docfile} EOS docdir = Dir.pwd docfile = File.join(docdir, 'sample.asciidoc') output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docdir' => docdir, 'docfile' => docfile} assert_xpath '//li[1]/p[text()="docdir: "]', output, 1 assert_xpath '//li[2]/p[text()="docfile: sample.asciidoc"]', output, 1 end test 'shows absolute docdir and docfile paths if safe mode is less than SERVER' do input = <<-EOS * docdir: {docdir} * docfile: {docfile} EOS docdir = Dir.pwd docfile = File.join(docdir, 'sample.asciidoc') output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => docdir, 'docfile' => docfile} assert_xpath %(//li[1]/p[text()="docdir: #{docdir}"]), output, 1 assert_xpath %(//li[2]/p[text()="docfile: #{docfile}"]), output, 1 end test 'assigns attribute defined in attribute reference with set prefix and value' do input = '{set:foo:bar}{foo}' output = render_embedded_string input assert_xpath '//p', output, 1 assert_xpath '//p[text()="bar"]', output, 1 end test 'assigns attribute defined in attribute reference with set prefix and no value' do input = "{set:foo}\n{foo}yes" output = render_embedded_string input assert_xpath '//p', output, 1 assert_xpath '//p[normalize-space(text())="yes"]', output, 1 end test 'assigns attribute defined in attribute reference with set prefix and empty value' do input = "{set:foo:}\n{foo}yes" output = render_embedded_string input assert_xpath '//p', output, 1 assert_xpath '//p[normalize-space(text())="yes"]', output, 1 end test 'unassigns attribute defined in attribute reference with set prefix' do input = <<-EOS :attribute-missing: drop-line :foo: {set:foo!} {foo}yes EOS output = render_embedded_string input assert_xpath '//p', output, 1 assert_xpath '//p/child::text()', output, 0 end end context "Intrinsic attributes" do test "substitute intrinsics" do Asciidoctor::INTRINSICS.each_pair do |key, value| html = render_string("Look, a {#{key}} is here") # can't use Nokogiri because it interprets the HTML entities and we can't match them assert_match(/Look, a #{Regexp.escape(value)} is here/, html) end end test "don't escape intrinsic substitutions" do html = render_string('happy{nbsp}together') assert_match(/happy together/, html) end test "escape special characters" do html = render_string('&') assert_match(/<node>&<\/node>/, html) end test 'creates counter' do input = <<-EOS {counter:mycounter} EOS doc = document_from_string input output = doc.render assert_equal 1, doc.attributes['mycounter'] assert_xpath '//p[text()="1"]', output, 1 end test 'creates counter silently' do input = <<-EOS {counter2:mycounter} EOS doc = document_from_string input output = doc.render assert_equal 1, doc.attributes['mycounter'] assert_xpath '//p[text()="1"]', output, 0 end test 'creates counter with numeric seed value' do input = <<-EOS {counter2:mycounter:10} EOS doc = document_from_string input doc.render assert_equal 10, doc.attributes['mycounter'] end test 'creates counter with character seed value' do input = <<-EOS {counter2:mycounter:A} EOS doc = document_from_string input doc.render assert_equal 'A', doc.attributes['mycounter'] end test 'increments counter with numeric value' do input = <<-EOS :mycounter: 1 {counter:mycounter} {mycounter} EOS doc = document_from_string input output = doc.render assert_equal 2, doc.attributes['mycounter'] assert_xpath '//p[text()="2"]', output, 2 end test 'increments counter with character value' do input = <<-EOS :mycounter: @ {counter:mycounter} {mycounter} EOS doc = document_from_string input output = doc.render assert_equal 'A', doc.attributes['mycounter'] assert_xpath '//p[text()="A"]', output, 2 end end context 'Block attributes' do test 'Positional attributes assigned to block' do input = <<-EOS [quote, author, source] ____ A famous quote. ____ EOS doc = document_from_string(input) qb = doc.blocks.first assert_equal 'quote', qb.style assert_equal 'author', qb.attr('attribution') assert_equal 'author', qb.attr(:attribution) assert_equal 'author', qb.attributes['attribution'] assert_equal 'source', qb.attributes['citetitle'] end test 'Normal substitutions are performed on single-quoted attributes' do input = <<-EOS [quote, author, 'http://wikipedia.org[source]'] ____ A famous quote. ____ EOS doc = document_from_string(input) qb = doc.blocks.first assert_equal 'quote', qb.style assert_equal 'author', qb.attr('attribution') assert_equal 'author', qb.attr(:attribution) assert_equal 'author', qb.attributes['attribution'] assert_equal 'source', qb.attributes['citetitle'] end test 'attribute list may begin with space' do input = <<-EOS [ quote] ____ A famous quote. ____ EOS doc = document_from_string input qb = doc.blocks.first assert_equal 'quote', qb.style end test 'attribute list may begin with comma' do input = <<-EOS [, author, source] ____ A famous quote. ____ EOS doc = document_from_string input qb = doc.blocks.first assert_equal 'quote', qb.style assert_equal 'author', qb.attributes['attribution'] assert_equal 'source', qb.attributes['citetitle'] end test 'first attribute in list may be double quoted' do input = <<-EOS ["quote", "author", "source", role="famous"] ____ A famous quote. ____ EOS doc = document_from_string input qb = doc.blocks.first assert_equal 'quote', qb.style assert_equal 'author', qb.attributes['attribution'] assert_equal 'source', qb.attributes['citetitle'] assert_equal 'famous', qb.attributes['role'] end test 'first attribute in list may be single quoted' do input = <<-EOS ['quote', 'author', 'source', role='famous'] ____ A famous quote. ____ EOS doc = document_from_string input qb = doc.blocks.first assert_equal 'quote', qb.style assert_equal 'author', qb.attributes['attribution'] assert_equal 'source', qb.attributes['citetitle'] assert_equal 'famous', qb.attributes['role'] end test 'role? returns true if role is assigned' do input = <<-EOS [role="lead"] A paragraph EOS doc = document_from_string input p = doc.blocks.first assert p.role? end test 'role? can check for exact role name match' do input = <<-EOS [role="lead"] A paragraph EOS doc = document_from_string input p = doc.blocks.first assert p.role?('lead') p2 = doc.blocks.last assert !p2.role?('final') end test 'has_role? can check for precense of role name' do input = <<-EOS [role="lead abstract"] A paragraph EOS doc = document_from_string input p = doc.blocks.first assert !p.role?('lead') assert p.has_role?('lead') end test 'roles returns array of role names' do input = <<-EOS [role="story lead"] A paragraph EOS doc = document_from_string input p = doc.blocks.first assert_equal ['story', 'lead'], p.roles end test 'roles returns empty array if role attribute is not set' do input = <<-EOS A paragraph EOS doc = document_from_string input p = doc.blocks.first assert_equal [], p.roles end test "Attribute substitutions are performed on attribute list before parsing attributes" do input = <<-EOS :lead: role="lead" [{lead}] A paragraph EOS doc = document_from_string(input) para = doc.blocks.first assert_equal 'lead', para.attributes['role'] end test 'id, role and options attributes can be specified on block style using shorthand syntax' do input = <<-EOS [normal#first.lead%step] A normal paragraph. EOS doc = document_from_string(input) para = doc.blocks.first assert_equal 'first', para.attributes['id'] assert_equal 'lead', para.attributes['role'] assert_equal 'step', para.attributes['options'] assert para.attributes.has_key?('step-option') end test 'multiple roles and options can be specified in block style using shorthand syntax' do input = <<-EOS [.role1%option1.role2%option2] Text EOS doc = document_from_string input para = doc.blocks.first assert_equal 'role1 role2', para.attributes['role'] assert_equal 'option1,option2', para.attributes['options'] assert para.attributes.has_key?('option1-option') assert para.attributes.has_key?('option2-option') end test 'option can be specified in first position of block style using shorthand syntax' do input = <<-EOS [%interactive] - [x] checked EOS doc = document_from_string input list = doc.blocks.first assert_equal 'interactive', list.attributes['options'] assert list.attributes.has_key?('interactive-option') assert list.attributes[1] == '%interactive' end test 'id and role attributes can be specified on section style using shorthand syntax' do input = <<-EOS [dedication#dedication.small] == Section Content. EOS output = render_embedded_string input assert_xpath '/div[@class="sect1 small"]', output, 1 assert_xpath '/div[@class="sect1 small"]/h2[@id="dedication"]', output, 1 end test "Block attributes are additive" do input = <<-EOS [id='foo'] [role='lead'] A paragraph. EOS doc = document_from_string(input) para = doc.blocks.first assert_equal 'foo', para.id assert_equal 'lead', para.attributes['role'] end test "Last wins for id attribute" do input = <<-EOS [[bar]] [[foo]] == Section paragraph [[baz]] [id='coolio'] === Section EOS doc = document_from_string(input) sec = doc.first_section assert_equal 'foo', sec.id subsec = sec.blocks.last assert_equal 'coolio', subsec.id end test 'block id above document title sets id on document' do input = <<-EOS [[reference]] Reference Manual ================ :css-signature: refguide preamble EOS doc = document_from_string input assert_equal 'reference', doc.id assert_equal 'refguide', doc.attr('css-signature') output = doc.render assert_xpath '//body[@id="reference"]', output, 1 end test "trailing block attributes tranfer to the following section" do input = <<-EOS [[one]] == Section One paragraph [[sub]] // try to mess this up! === Sub-section paragraph [role='classy'] //// block comment //// == Section Two content EOS doc = document_from_string(input) section_one = doc.blocks.first assert_equal 'one', section_one.id subsection = section_one.blocks.last assert_equal 'sub', subsection.id section_two = doc.blocks.last assert_equal 'classy', section_two.attr(:role) end end end asciidoctor-0.1.4/test/blocks_test.rb000066400000000000000000001703411221220517700176440ustar00rootroot00000000000000require 'test_helper' require 'pathname' context "Blocks" do context 'Line Breaks' do test "ruler" do output = render_string("'''") assert_xpath '//*[@id="content"]/hr', output, 1 assert_xpath '//*[@id="content"]/*', output, 1 end test "ruler between blocks" do output = render_string("Block above\n\n'''\n\nBlock below") assert_xpath '//*[@id="content"]/hr', output, 1 assert_xpath '//*[@id="content"]/hr/preceding-sibling::*', output, 1 assert_xpath '//*[@id="content"]/hr/following-sibling::*', output, 1 end test "page break" do output = render_embedded_string("page 1\n\n<<<\n\npage 2") assert_xpath '/*[translate(@style, ";", "")="page-break-after: always"]', output, 1 assert_xpath '/*[translate(@style, ";", "")="page-break-after: always"]/preceding-sibling::div/p[text()="page 1"]', output, 1 assert_xpath '/*[translate(@style, ";", "")="page-break-after: always"]/following-sibling::div/p[text()="page 2"]', output, 1 end end context 'Comments' do test 'line comment between paragraphs offset by blank lines' do input = <<-EOS first paragraph // line comment second paragraph EOS output = render_embedded_string input assert_no_match(/line comment/, output) assert_xpath '//p', output, 2 end test 'adjacent line comment between paragraphs' do input = <<-EOS first line // line comment second line EOS output = render_embedded_string input assert_no_match(/line comment/, output) assert_xpath '//p', output, 1 assert_xpath "//p[1][text()='first line\nsecond line']", output, 1 end test 'comment block between paragraphs offset by blank lines' do input = <<-EOS first paragraph //// block comment //// second paragraph EOS output = render_embedded_string input assert_no_match(/block comment/, output) assert_xpath '//p', output, 2 end test 'adjacent comment block between paragraphs' do input = <<-EOS first paragraph //// block comment //// second paragraph EOS output = render_embedded_string input assert_no_match(/block comment/, output) assert_xpath '//p', output, 2 end test "can render with block comment at end of document with trailing endlines" do input = <<-EOS paragraph //// block comment //// EOS output = render_embedded_string input assert_no_match(/block comment/, output) end test "trailing endlines after block comment at end of document does not create paragraph" do input = <<-EOS paragraph //// block comment //// EOS d = document_from_string input assert_equal 1, d.blocks.size assert_xpath '//p', d.render, 1 end test 'line starting with three slashes should not be line comment' do input = <<-EOS /// not a line comment EOS output = render_embedded_string input assert !output.strip.empty?, "Line should be emitted => #{input.rstrip}" end test 'preprocessor directives should not be processed within comment block within block metadata' do input = <<-EOS .sample title //// ifdef::asciidoctor[////] //// line should be rendered EOS output = render_embedded_string input assert_xpath '//p[text() = "line should be rendered"]', output, 1 end test 'preprocessor directives should not be processed within comment block' do input = <<-EOS dummy line //// ifdef::asciidoctor[////] //// line should be rendered EOS output = render_embedded_string input assert_xpath '//p[text() = "line should be rendered"]', output, 1 end # WARNING if first line of content is a directive, it will get interpretted before we know it's a comment block # it happens because we always look a line ahead...not sure what we can do about it test 'preprocessor directives should not be processed within comment open block' do input = <<-EOS [comment] -- first line of comment ifdef::asciidoctor[--] line should not be rendered -- EOS output = render_embedded_string input assert_xpath '//p', output, 0 end # WARNING if first line of content is a directive, it will get interpretted before we know it's a comment block # it happens because we always look a line ahead...not sure what we can do about it test 'preprocessor directives should not be processed within comment paragraph' do input = <<-EOS [comment] first line of content ifdef::asciidoctor[////] this line should be rendered EOS output = render_embedded_string input assert_xpath '//p[text() = "this line should be rendered"]', output, 1 end test 'comment style on open block should only skip block' do input = <<-EOS [comment] -- skip this block -- not this text EOS result = render_embedded_string input assert_xpath '//p', result, 1 assert_xpath '//p[text()="not this text"]', result, 1 end test 'comment style on paragraph should only skip paragraph' do input = <<-EOS [comment] skip this paragraph not this text EOS result = render_embedded_string input assert_xpath '//p', result, 1 assert_xpath '//p[text()="not this text"]', result, 1 end test 'comment style on paragraph should not cause adjacent block to be skipped' do input = <<-EOS [comment] skip this paragraph [example] not this text EOS result = render_embedded_string input assert_xpath '/*[@class="exampleblock"]', result, 1 assert_xpath '/*[@class="exampleblock"]//*[normalize-space(text())="not this text"]', result, 1 end end context 'Quote and Verse Blocks' do test 'quote block with no attribution' do input = <<-EOS ____ A famous quote. ____ EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > .attribution', output, 0 assert_xpath '//*[@class = "quoteblock"]//p[text() = "A famous quote."]', output, 1 end test 'quote block with attribution' do input = <<-EOS [quote, Famous Person, Famous Book (1999)] ____ A famous quote. ____ EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > .attribution', output, 1 assert_css '.quoteblock > .attribution > cite', output, 1 assert_css '.quoteblock > .attribution > cite + br', output, 1 assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Book (1999)"]', output, 1 attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1 author = attribution.children.last assert_equal "#{expand_entity 8212} Famous Person", author.text.strip end test 'quote block with attribute and id and role shorthand' do input = <<-EOS [quote#think.big, Donald Trump] ____ As long as your going to be thinking anyway, think big. ____ EOS output = render_embedded_string input assert_css '.quoteblock', output, 1 assert_css '#think.quoteblock.big', output, 1 assert_css '.quoteblock > .attribution', output, 1 end test 'quote block with complex content' do input = <<-EOS ____ A famous quote. NOTE: _That_ was inspiring. ____ EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph', output, 1 assert_css '.quoteblock > blockquote > .paragraph + .admonitionblock', output, 1 end test 'quote block using air quotes with no attribution' do input = <<-EOS "" A famous quote. "" EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > .attribution', output, 0 assert_xpath '//*[@class = "quoteblock"]//p[text() = "A famous quote."]', output, 1 end test 'markdown-style quote block with single paragraph and no attribution' do input = <<-EOS > A famous quote. > Some more inspiring words. EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > .attribution', output, 0 assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 end test 'lazy markdown-style quote block with single paragraph and no attribution' do input = <<-EOS > A famous quote. Some more inspiring words. EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > .attribution', output, 0 assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 end test 'markdown-style quote block with multiple paragraphs and no attribution' do input = <<-EOS > A famous quote. > > Some more inspiring words. EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 2 assert_css '.quoteblock > .attribution', output, 0 assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1 assert_xpath %((//*[@class = "quoteblock"]//p)[2][text() = "Some more inspiring words."]), output, 1 end test 'markdown-style quote block with multiple blocks and no attribution' do input = <<-EOS > A famous quote. > > NOTE: Some more inspiring words. EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_css '.quoteblock > blockquote > .admonitionblock', output, 1 assert_css '.quoteblock > .attribution', output, 0 assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1 assert_xpath %((//*[@class = "quoteblock"]//*[@class = "admonitionblock note"]//*[@class="content"])[1][normalize-space(text()) = "Some more inspiring words."]), output, 1 end test 'markdown-style quote block with single paragraph and attribution' do input = <<-EOS > A famous quote. > Some more inspiring words. > -- Famous Person, Famous Source, Volume 1 (1999) EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 assert_css '.quoteblock > .attribution', output, 1 assert_css '.quoteblock > .attribution > cite', output, 1 assert_css '.quoteblock > .attribution > cite + br', output, 1 assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source, Volume 1 (1999)"]', output, 1 attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1 author = attribution.children.last assert_equal "#{expand_entity 8212} Famous Person", author.text.strip end test 'quoted paragraph-style quote block with attribution' do input = <<-EOS "A famous quote. Some more inspiring words." -- Famous Person, Famous Source, Volume 1 (1999) EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_xpath %(//*[@class = "quoteblock"]/blockquote[normalize-space(text()) = "A famous quote. Some more inspiring words."]), output, 1 assert_css '.quoteblock > .attribution', output, 1 assert_css '.quoteblock > .attribution > cite', output, 1 assert_css '.quoteblock > .attribution > cite + br', output, 1 assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source, Volume 1 (1999)"]', output, 1 attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1 author = attribution.children.last assert_equal "#{expand_entity 8212} Famous Person", author.text.strip end test 'single-line verse block without attribution' do input = <<-EOS [verse] ____ A famous verse. ____ EOS output = render_string input assert_css '.verseblock', output, 1 assert_css '.verseblock > pre', output, 1 assert_css '.verseblock > .attribution', output, 0 assert_css '.verseblock p', output, 0 assert_xpath '//*[@class = "verseblock"]/pre[normalize-space(text()) = "A famous verse."]', output, 1 end test 'single-line verse block with attribution' do input = <<-EOS [verse, Famous Poet, Famous Poem] ____ A famous verse. ____ EOS output = render_string input assert_css '.verseblock', output, 1 assert_css '.verseblock p', output, 0 assert_css '.verseblock > pre', output, 1 assert_css '.verseblock > .attribution', output, 1 assert_css '.verseblock > .attribution > cite', output, 1 assert_css '.verseblock > .attribution > cite + br', output, 1 assert_xpath '//*[@class = "verseblock"]/*[@class = "attribution"]/cite[text() = "Famous Poem"]', output, 1 attribution = xmlnodes_at_xpath '//*[@class = "verseblock"]/*[@class = "attribution"]', output, 1 author = attribution.children.last assert_equal "#{expand_entity 8212} Famous Poet", author.text.strip end test 'multi-stanza verse block' do input = <<-EOS [verse] ____ A famous verse. Stanza two. ____ EOS output = render_string input assert_xpath '//*[@class = "verseblock"]', output, 1 assert_xpath '//*[@class = "verseblock"]/pre', output, 1 assert_xpath '//*[@class = "verseblock"]//p', output, 0 assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "A famous verse.")]', output, 1 assert_xpath '//*[@class = "verseblock"]/pre[contains(text(), "Stanza two.")]', output, 1 end test 'verse block does not contain block elements' do input = <<-EOS [verse] ____ A famous verse. .... not a literal .... ____ EOS output = render_string input assert_css '.verseblock', output, 1 assert_css '.verseblock > pre', output, 1 assert_css '.verseblock p', output, 0 assert_css '.verseblock .literalblock', output, 0 end test 'verse should only have specialcharacters subs' do input = <<-EOS [verse] ____ A famous verse ____ EOS verse = block_from_string input assert_equal [:specialcharacters], verse.subs end test 'should not recognize callouts in a verse' do input = <<-EOS [verse] ____ La la la <1> ____ <1> Not pointing to a callout EOS output = render_embedded_string input assert_xpath '//pre[text()="La la la <1>"]', output, 1 end end context "Example Blocks" do test "can render example block" do input = <<-EOS ==== This is an example of an example block. How crazy is that? ==== EOS output = render_string input assert_xpath '//*[@class="exampleblock"]//p', output, 2 end test "assigns sequential numbered caption to example block with title" do input = <<-EOS .Writing Docs with AsciiDoc ==== Here's how you write AsciiDoc. You just write. ==== .Writing Docs with DocBook ==== Here's how you write DocBook. You futz with XML. ==== EOS doc = document_from_string input output = doc.render assert_xpath '(//*[@class="exampleblock"])[1]/*[@class="title"][text()="Example 1. Writing Docs with AsciiDoc"]', output, 1 assert_xpath '(//*[@class="exampleblock"])[2]/*[@class="title"][text()="Example 2. Writing Docs with DocBook"]', output, 1 assert_equal 2, doc.attributes['example-number'] end test "assigns sequential character caption to example block with title" do input = <<-EOS :example-number: @ .Writing Docs with AsciiDoc ==== Here's how you write AsciiDoc. You just write. ==== .Writing Docs with DocBook ==== Here's how you write DocBook. You futz with XML. ==== EOS doc = document_from_string input output = doc.render assert_xpath '(//*[@class="exampleblock"])[1]/*[@class="title"][text()="Example A. Writing Docs with AsciiDoc"]', output, 1 assert_xpath '(//*[@class="exampleblock"])[2]/*[@class="title"][text()="Example B. Writing Docs with DocBook"]', output, 1 assert_equal 'B', doc.attributes['example-number'] end test "explicit caption is used if provided" do input = <<-EOS [caption="Look! "] .Writing Docs with AsciiDoc ==== Here's how you write AsciiDoc. You just write. ==== EOS doc = document_from_string input output = doc.render assert_xpath '(//*[@class="exampleblock"])[1]/*[@class="title"][text()="Look! Writing Docs with AsciiDoc"]', output, 1 assert !doc.attributes.has_key?('example-number') end test 'explicit caption is set on block even if block has no title' do input = <<-EOS [caption="Look!"] ==== Just write. ==== EOS doc = document_from_string input assert_equal 'Look!', doc.blocks.first.caption output = doc.render assert_no_match(/Look/, output) end test 'automatic caption can be turned off and on and modified' do input = <<-EOS .first example ==== an example ==== :caption: .second example ==== another example ==== :caption!: :example-caption: Exhibit .third example ==== yet another example ==== EOS output = render_embedded_string input assert_xpath '/*[@class="exampleblock"]', output, 3 assert_xpath '(/*[@class="exampleblock"])[1]/*[@class="title"][starts-with(text(), "Example ")]', output, 1 assert_xpath '(/*[@class="exampleblock"])[2]/*[@class="title"][text()="second example"]', output, 1 assert_xpath '(/*[@class="exampleblock"])[3]/*[@class="title"][starts-with(text(), "Exhibit ")]', output, 1 end end context 'Admonition Blocks' do test 'caption block-level attribute should be used as caption' do input = <<-EOS :tip-caption: Pro Tip [caption="Pro Tip"] TIP: Override the caption of an admonition block using an attribute entry EOS output = render_embedded_string input assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1 end test 'can override caption of admonition block using document attribute' do input = <<-EOS :tip-caption: Pro Tip TIP: Override the caption of an admonition block using an attribute entry EOS output = render_embedded_string input assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Pro Tip"]', output, 1 end test 'blank caption document attribute should not blank admonition block caption' do input = <<-EOS :caption: TIP: Override the caption of an admonition block using an attribute entry EOS output = render_embedded_string input assert_xpath '/*[@class="admonitionblock tip"]//*[@class="icon"]/*[@class="title"][text()="Tip"]', output, 1 end end context "Preformatted Blocks" do test 'should separate adjacent paragraphs and listing into blocks' do input = <<-EOS paragraph 1 ---- listing content ---- paragraph 2 EOS output = render_embedded_string input assert_xpath '/*[@class="paragraph"]/p', output, 2 assert_xpath '/*[@class="listingblock"]', output, 1 assert_xpath '(/*[@class="paragraph"]/following-sibling::*)[1][@class="listingblock"]', output, 1 end test "should preserve endlines in literal block" do input = <<-EOS .... line one line two line three .... EOS [true, false].each {|compact| output = render_string input, :compact => compact assert_xpath '//pre', output, 1 assert_xpath '//pre/text()', output, 1 text = xmlnodes_at_xpath('//pre/text()', output, 1).text lines = text.lines.entries assert_equal 5, lines.size expected = "line one\n\nline two\n\nline three".lines.entries assert_equal expected, lines blank_lines = output.scan(/\n[[:blank:]]*\n/).size if compact assert_equal 2, blank_lines else assert blank_lines >= 2 end } end test "should preserve endlines in listing block" do input = <<-EOS [source] ---- line one line two line three ---- EOS [true, false].each {|compact| output = render_string input, :compact => compact assert_xpath '//pre/code', output, 1 assert_xpath '//pre/code/text()', output, 1 text = xmlnodes_at_xpath('//pre/code/text()', output, 1).text lines = text.lines.entries assert_equal 5, lines.size expected = "line one\n\nline two\n\nline three".lines.entries assert_equal expected, lines blank_lines = output.scan(/\n[[:blank:]]*\n/).size if compact assert_equal 2, blank_lines else assert blank_lines >= 2 end } end test "should preserve endlines in verse block" do input = <<-EOS [verse] ____ line one line two line three ____ EOS [true, false].each {|compact| output = render_string input, :compact => compact assert_xpath '//*[@class="verseblock"]/pre', output, 1 assert_xpath '//*[@class="verseblock"]/pre/text()', output, 1 text = xmlnodes_at_xpath('//*[@class="verseblock"]/pre/text()', output, 1).text lines = text.lines.entries assert_equal 5, lines.size expected = "line one\n\nline two\n\nline three".lines.entries assert_equal expected, lines blank_lines = output.scan(/\n[[:blank:]]*\n/).size if compact assert_equal 2, blank_lines else assert blank_lines >= 2 end } end test 'should not compact nested document twice' do input = <<-EOS |=== a|.... line one line two line three .... |=== EOS output = render_string input, :compact => true assert_xpath %(//pre[text() = "line one\n\nline two\n\nline three"]), output, 1 end test 'should process block with CRLF endlines' do input = <<-EOS [source]\r ----\r source line 1\r source line 2\r ----\r EOS output = render_embedded_string input assert_no_match(/\[source\]/, output) assert_xpath '/*[@class="listingblock"]//pre', output, 1 assert_xpath '/*[@class="listingblock"]//pre/code', output, 1 assert_xpath %(/*[@class="listingblock"]//pre/code[text()="source line 1\nsource line 2"]), output, 1 end test 'should remove block indent if indent attribute is 0' do input = <<-EOS [indent="0"] ---- def names @names.split ' ' end ---- EOS expected = <<-EOS def names @names.split ' ' end EOS output = render_embedded_string input assert_css 'pre', output, 1 assert_css '.listingblock pre', output, 1 result = xmlnodes_at_xpath('//pre', output, 1).text assert_equal expected.chomp, result end test 'should set block indent to value specified by indent attribute' do input = <<-EOS [indent="1"] ---- def names @names.split ' ' end ---- EOS expected = <<-EOS def names @names.split ' ' end EOS output = render_embedded_string input assert_css 'pre', output, 1 assert_css '.listingblock pre', output, 1 result = xmlnodes_at_xpath('//pre', output, 1).text assert_equal expected.chomp, result end test 'literal block should honor nowrap option' do input = <<-EOS [options="nowrap"] ---- Do not wrap me if I get too long. ---- EOS output = render_embedded_string input assert_css 'pre.nowrap', output, 1 end test 'literal block should set nowrap class if prewrap document attribute is disabled' do input = <<-EOS :prewrap!: ---- Do not wrap me if I get too long. ---- EOS output = render_embedded_string input assert_css 'pre.nowrap', output, 1 end test 'literal block should honor explicit subs list' do input = <<-EOS [subs="verbatim,quotes"] ---- Map *attributes*; //<1> ---- EOS block = block_from_string input assert_equal [:specialcharacters,:callouts,:quotes], block.subs output = block.render assert output.include?('Map<String, String> attributes;') assert_xpath '//pre/b[text()="(1)"]', output, 1 end test 'should be able to disable callouts for literal block' do input = <<-EOS [subs="specialcharacters"] ---- No callout here <1> ---- EOS block = block_from_string input assert_equal [:specialcharacters], block.subs output = block.render assert_xpath '//pre/b[text()="(1)"]', output, 0 end test 'listing block should honor explicit subs list' do input = <<-EOS [subs="specialcharacters,quotes"] ---- $ *python functional_tests.py* Traceback (most recent call last): File "functional_tests.py", line 4, in assert 'Django' in browser.title AssertionError ---- EOS output = render_embedded_string input assert_css '.listingblock pre', output, 1 assert_css '.listingblock pre strong', output, 1 assert_css '.listingblock pre em', output, 1 input2 = <<-EOS [subs="specialcharacters,macros"] ---- $ pass:quotes[*python functional_tests.py*] Traceback (most recent call last): File "functional_tests.py", line 4, in assert pass:quotes['Django'] in browser.title AssertionError ---- EOS output2 = render_embedded_string input2 # FIXME JRuby is adding extra trailing endlines in the second document, # for now, rstrip is necessary assert_equal output.rstrip, output2.rstrip end test 'listing block without title should generate screen element in docbook' do input = <<-EOS ---- listing block ---- EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/screen[text()="listing block"]', output, 1 end test 'listing block with title should generate screen element inside formalpara element in docbook' do input = <<-EOS .title ---- listing block ---- EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/formalpara', output, 1 assert_xpath '/formalpara/title[text()="title"]', output, 1 assert_xpath '/formalpara/para/screen[text()="listing block"]', output, 1 end test 'source block with no title or language should generate screen element in docbook' do input = <<-EOS [source] ---- listing block ---- EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/screen[text()="listing block"]', output, 1 end test 'source block with title and no language should generate screen element inside formalpara element in docbook' do input = <<-EOS [source] .title ---- listing block ---- EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/formalpara', output, 1 assert_xpath '/formalpara/title[text()="title"]', output, 1 assert_xpath '/formalpara/para/screen[text()="listing block"]', output, 1 end end context "Open Blocks" do test "can render open block" do input = <<-EOS -- This is an open block. It can span multiple lines. -- EOS output = render_string input assert_xpath '//*[@class="openblock"]//p', output, 2 end test "open block can contain another block" do input = <<-EOS -- This is an open block. It can span multiple lines. ____ It can hold great quotes like this one. ____ -- EOS output = render_string input assert_xpath '//*[@class="openblock"]//p', output, 3 assert_xpath '//*[@class="openblock"]//*[@class="quoteblock"]', output, 1 end end context 'Passthrough Blocks' do test 'can parse a passthrough block' do input = <<-EOS ++++ This is a passthrough block. ++++ EOS block = block_from_string input assert !block.nil? assert_equal 1, block.lines.size assert_equal 'This is a passthrough block.', block.source end test 'performs passthrough subs on a passthrough block' do input = <<-EOS :type: passthrough ++++ This is a '{type}' block. http://asciidoc.org ++++ EOS expected = %(This is a 'passthrough' block.\nhttp://asciidoc.org) output = render_embedded_string input assert_equal expected, output.strip end test 'passthrough block honors explicit subs list' do input = <<-EOS :type: passthrough [subs="attributes, quotes"] ++++ This is a '{type}' block. http://asciidoc.org ++++ EOS expected = %(This is a passthrough block.\nhttp://asciidoc.org) output = render_embedded_string input assert_equal expected, output.strip end end context 'Metadata' do test 'block title above section gets carried over to first block in section' do input = <<-EOS .Title == Section paragraph EOS output = render_string input assert_xpath '//*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="paragraph"]/*[@class="title"][text() = "Title"]', output, 1 assert_xpath '//*[@class="paragraph"]/p[text() = "paragraph"]', output, 1 end test 'block title above document title demotes document title to a section title' do input = <<-EOS .Block title = Section Title section paragraph EOS output, errors = nil redirect_streams do |stdout, stderr| output = render_string input errors = stderr.string end assert_xpath '//*[@id="header"]/*', output, 0 assert_xpath '//*[@id="preamble"]/*', output, 0 assert_xpath '//*[@id="content"]/h1[text()="Section Title"]', output, 1 assert_xpath '//*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="paragraph"]/*[@class="title"][text()="Block title"]', output, 1 assert !errors.empty? assert_match(/only book doctypes can contain level 0 sections/, errors) end test 'block title above document title gets carried over to first block in first section if no preamble' do input = <<-EOS .Block title = Document Title == First Section paragraph EOS output = render_string input assert_xpath '//*[@class="sect1"]//*[@class="paragraph"]/*[@class="title"][text() = "Block title"]', output, 1 end test 'empty attribute list should not appear in output' do input = <<-EOS [] -- Block content -- EOS output = render_embedded_string input assert output.include?('Block content') assert !output.include?('[]') end test 'empty block anchor should not appear in output' do input = <<-EOS [[]] -- Block content -- EOS output = render_embedded_string input assert output.include?('Block content') assert !output.include?('[[]]') end end context 'Images' do test 'can render block image with alt text defined in macro' do input = <<-EOS image::images/tiger.png[Tiger] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end test 'can render block image with alt text defined in macro containing escaped square bracket' do input = <<-EOS image::images/tiger.png[A [Bengal\\] Tiger] EOS output = render_string input img = xmlnodes_at_xpath '//img', output, 1 assert_equal 'A [Bengal] Tiger', img.attr('alt').value end test 'can render block image with alt text defined in block attribute above macro' do input = <<-EOS [Tiger] image::images/tiger.png[] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end test 'alt text in macro overrides alt text above macro' do input = <<-EOS [Alt Text] image::images/tiger.png[Tiger] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end test "can render block image with auto-generated alt text" do input = <<-EOS image::images/tiger.png[] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="tiger"]', output, 1 end test "can render block image with alt text and height and width" do input = <<-EOS image::images/tiger.png[Tiger, 200, 300] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"][@width="200"][@height="300"]', output, 1 end test "can render block image with link" do input = <<-EOS image::images/tiger.png[Tiger, link='http://en.wikipedia.org/wiki/Tiger'] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//a[@class="image"][@href="http://en.wikipedia.org/wiki/Tiger"]/img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end test "can render block image with caption" do input = <<-EOS .The AsciiDoc Tiger image::images/tiger.png[Tiger] EOS doc = document_from_string input output = doc.render assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 assert_xpath '//*[@class="imageblock"]/*[@class="title"][text() = "Figure 1. The AsciiDoc Tiger"]', output, 1 assert_equal 1, doc.attributes['figure-number'] end test 'can render block image with explicit caption' do input = <<-EOS [caption="Voila! "] .The AsciiDoc Tiger image::images/tiger.png[Tiger] EOS doc = document_from_string input output = doc.render assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 assert_xpath '//*[@class="imageblock"]/*[@class="title"][text() = "Voila! The AsciiDoc Tiger"]', output, 1 assert !doc.attributes.has_key?('figure-number') end test 'keeps line unprocessed if image target is missing attribute reference and attribute-missing is skip' do input = <<-EOS :attribute-missing: skip image::{bogus}[] EOS output = render_embedded_string input assert output.include?('image::{bogus}[]') end test 'drops line if image target is missing attribute reference and attribute-missing is drop' do input = <<-EOS :attribute-missing: drop image::{bogus}[] EOS output = render_embedded_string input assert output.strip.empty? end test 'drops line if image target is missing attribute reference and attribute-missing is drop-line' do input = <<-EOS :attribute-missing: drop-line image::{bogus}[] EOS output = render_embedded_string input assert output.strip.empty? end test 'dropped image does not break processing of following section and attribute-missing is drop-line' do input = <<-EOS :attribute-missing: drop-line image::{bogus}[] == Section Title EOS output = render_embedded_string input assert_css 'img', output, 0 assert_css 'h2', output, 1 assert !output.include?('== Section Title') end test 'should pass through image that references uri' do input = <<-EOS :imagesdir: images image::http://asciidoc.org/images/tiger.png[Tiger] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="http://asciidoc.org/images/tiger.png"][@alt="Tiger"]', output, 1 end test 'can resolve image relative to imagesdir' do input = <<-EOS :imagesdir: images image::tiger.png[Tiger] EOS output = render_string input assert_xpath '//*[@class="imageblock"]//img[@src="images/tiger.png"][@alt="Tiger"]', output, 1 end test 'embeds base64-encoded data uri for image when data-uri attribute is set' do input = <<-EOS :data-uri: :imagesdir: fixtures image::dot.gif[Dot] EOS doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} assert_equal 'fixtures', doc.attributes['imagesdir'] output = doc.render assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1 end # this test will cause a warning to be printed to the console (until we have a message facility) test 'cleans reference to ancestor directories in imagesdir before reading image if safe mode level is at least SAFE' do input = <<-EOS :data-uri: :imagesdir: ../..//fixtures/./../../fixtures image::dot.gif[Dot] EOS doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} assert_equal '../..//fixtures/./../../fixtures', doc.attributes['imagesdir'] output = doc.render # image target resolves to fixtures/dot.gif relative to docdir (which is explicitly set to the directory of this file) # the reference cannot fall outside of the document directory in safe mode assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1 end test 'cleans reference to ancestor directories in target before reading image if safe mode level is at least SAFE' do input = <<-EOS :data-uri: :imagesdir: ./ image::../..//fixtures/./../../fixtures/dot.gif[Dot] EOS doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} assert_equal './', doc.attributes['imagesdir'] output = doc.render # image target resolves to fixtures/dot.gif relative to docdir (which is explicitly set to the directory of this file) # the reference cannot fall outside of the document directory in safe mode assert_xpath '//*[@class="imageblock"]//img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Dot"]', output, 1 end end context 'Media' do test 'should detect and render video macro' do input = <<-EOS video::cats-vs-dogs.avi[] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[src="cats-vs-dogs.avi"]', output, 1 end test 'should detect and render video macro with positional attributes for poster and dimensions' do input = <<-EOS video::cats-vs-dogs.avi[cats-and-dogs.png, 200, 300] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[src="cats-vs-dogs.avi"]', output, 1 assert_css 'video[poster="cats-and-dogs.png"]', output, 1 assert_css 'video[width="200"]', output, 1 assert_css 'video[height="300"]', output, 1 end test 'video macro should honor all options' do input = <<-EOS video::cats-vs-dogs.avi[options="autoplay,nocontrols,loop"] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[autoplay]', output, 1 assert_css 'video:not([controls])', output, 1 assert_css 'video[loop]', output, 1 end test 'video macro should use imagesdir attribute to resolve target and poster' do input = <<-EOS :imagesdir: assets video::cats-vs-dogs.avi[cats-and-dogs.png, 200, 300] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[src="assets/cats-vs-dogs.avi"]', output, 1 assert_css 'video[poster="assets/cats-and-dogs.png"]', output, 1 assert_css 'video[width="200"]', output, 1 assert_css 'video[height="300"]', output, 1 end test 'video macro should not use imagesdir attribute to resolve target if target is a URL' do input = <<-EOS :imagesdir: assets video::http://example.org/videos/cats-vs-dogs.avi[] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[src="http://example.org/videos/cats-vs-dogs.avi"]', output, 1 end test 'video macro should output custom HTML with iframe for vimeo service' do input = <<-EOS video::67480300[vimeo, 400, 300, start=60, options=autoplay] EOS output = render_embedded_string input assert_css 'video', output, 0 assert_css 'iframe', output, 1 assert_css 'iframe[src="//player.vimeo.com/video/67480300#at=60?autoplay=1"]', output, 1 assert_css 'iframe[width="400"]', output, 1 assert_css 'iframe[height="300"]', output, 1 end test 'video macro should output custom HTML with iframe for youtube service' do input = <<-EOS video::rPQoq7ThGAU[youtube, 640, 360, start=60, options=autoplay] EOS output = render_embedded_string input assert_css 'video', output, 0 assert_css 'iframe', output, 1 assert_css 'iframe[src="//www.youtube.com/embed/rPQoq7ThGAU?rel=0&start=60&autoplay=1"]', output, 1 assert_css 'iframe[width="640"]', output, 1 assert_css 'iframe[height="360"]', output, 1 end test 'should detect and render audio macro' do input = <<-EOS audio::podcast.mp3[] EOS output = render_embedded_string input assert_css 'audio', output, 1 assert_css 'audio[src="podcast.mp3"]', output, 1 end test 'audio macro should use imagesdir attribute to resolve target' do input = <<-EOS :imagesdir: assets audio::podcast.mp3[] EOS output = render_embedded_string input assert_css 'audio', output, 1 assert_css 'audio[src="assets/podcast.mp3"]', output, 1 end test 'audio macro should not use imagesdir attribute to resolve target if target is a URL' do input = <<-EOS :imagesdir: assets video::http://example.org/podcast.mp3[] EOS output = render_embedded_string input assert_css 'video', output, 1 assert_css 'video[src="http://example.org/podcast.mp3"]', output, 1 end test 'audio macro should honor all options' do input = <<-EOS audio::podcast.mp3[options="autoplay,nocontrols,loop"] EOS output = render_embedded_string input assert_css 'audio', output, 1 assert_css 'audio[autoplay]', output, 1 assert_css 'audio:not([controls])', output, 1 assert_css 'audio[loop]', output, 1 end end context 'Admonition icons' do test 'can resolve icon relative to default iconsdir' do input = <<-EOS :icons: [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :safe => Asciidoctor::SafeMode::SERVER assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="./images/icons/tip.png"][@alt="Tip"]', output, 1 end test 'can resolve icon relative to custom iconsdir' do input = <<-EOS :icons: :iconsdir: icons [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :safe => Asciidoctor::SafeMode::SERVER assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="icons/tip.png"][@alt="Tip"]', output, 1 end test 'embeds base64-encoded data uri of icon when data-uri attribute is set and safe mode level is less than SECURE' do input = <<-EOS :icons: :iconsdir: fixtures :icontype: gif :data-uri: [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1 end test 'does not embed base64-encoded data uri of icon when safe mode level is SECURE or greater' do input = <<-EOS :icons: :iconsdir: fixtures :icontype: gif :data-uri: [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :attributes => {'icons' => ''} assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="fixtures/tip.gif"][@alt="Tip"]', output, 1 end test 'cleans reference to ancestor directories before reading icon if safe mode level is at least SAFE' do input = <<-EOS :icons: :iconsdir: ../fixtures :icontype: gif :data-uri: [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'docdir' => File.dirname(__FILE__)} assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/img[@src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="][@alt="Tip"]', output, 1 end test 'should import Font Awesome and use font-based icons when value of icons attribute is font' do input = <<-EOS :icons: font [TIP] You can use icons for admonitions by setting the 'icons' attribute. EOS output = render_string input, :safe => Asciidoctor::SafeMode::SERVER assert_css 'html > head > link[rel="stylesheet"][href="http://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.min.css"]', output, 1 assert_xpath '//*[@class="admonitionblock tip"]//*[@class="icon"]/i[@class="icon-tip"]', output, 1 end end context 'Image paths' do test 'restricts access to ancestor directories when safe mode level is at least SAFE' do input = <<-EOS image::asciidoctor.png[Asciidoctor] EOS basedir = File.expand_path File.dirname(__FILE__) block = block_from_string input, :attributes => {'docdir' => basedir} doc = block.document assert doc.safe >= Asciidoctor::SafeMode::SAFE assert_equal File.join(basedir, 'images'), block.normalize_asset_path('images') assert_equal File.join(basedir, 'etc/images'), block.normalize_asset_path("#{disk_root}etc/images") assert_equal File.join(basedir, 'images'), block.normalize_asset_path('../../images') end test 'does not restrict access to ancestor directories when safe mode is disabled' do input = <<-EOS image::asciidoctor.png[Asciidoctor] EOS basedir = File.expand_path File.dirname(__FILE__) block = block_from_string input, :safe => Asciidoctor::SafeMode::UNSAFE, :attributes => {'docdir' => basedir} doc = block.document assert doc.safe == Asciidoctor::SafeMode::UNSAFE assert_equal File.join(basedir, 'images'), block.normalize_asset_path('images') absolute_path = "#{disk_root}etc/images" assert_equal absolute_path, block.normalize_asset_path(absolute_path) assert_equal File.expand_path(File.join(basedir, '../../images')), block.normalize_asset_path('../../images') end end context 'Source code' do test 'should support fenced code block using backticks' do input = <<-EOS ``` puts "Hello, World!" ``` EOS output = render_embedded_string input assert_css '.listingblock', output, 1 assert_css '.listingblock pre code', output, 1 assert_css '.listingblock pre code:not([class])', output, 1 end test 'should support fenced code block using tildes' do input = <<-EOS ~~~ puts "Hello, World!" ~~~ EOS output = render_embedded_string input assert_css '.listingblock', output, 1 assert_css '.listingblock pre code', output, 1 assert_css '.listingblock pre code:not([class])', output, 1 end test 'should not recognize fenced code blocks with more than three delimiters' do input = <<-EOS ````ruby puts "Hello, World!" ```` ~~~~ javascript alert("Hello, World!") ~~~~ EOS output = render_embedded_string input assert_css '.listingblock', output, 0 end test 'should support fenced code blocks with languages' do input = <<-EOS ```ruby puts "Hello, World!" ``` ~~~ javascript alert("Hello, World!") ~~~ EOS output = render_embedded_string input assert_css '.listingblock', output, 2 assert_css '.listingblock pre code.ruby', output, 1 assert_css '.listingblock pre code.javascript', output, 1 end test 'should support fenced code blocks with languages and numbering' do input = <<-EOS ```ruby,numbered puts "Hello, World!" ``` ~~~ javascript, numbered alert("Hello, World!") ~~~ EOS output = render_embedded_string input assert_css '.listingblock', output, 2 assert_css '.listingblock pre code.ruby', output, 1 assert_css '.listingblock pre code.javascript', output, 1 end test 'should highlight source if source-highlighter attribute is coderay' do input = <<-EOS :source-highlighter: coderay [source, ruby] ---- require 'coderay' html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table) ---- EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :linkcss_default => true assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1 assert_match(/\.CodeRay \{/, output) end test 'should replace callout marks but not highlight them if source-highlighter attribute is coderay' do input = <<-EOS :source-highlighter: coderay [source, ruby] ---- require 'coderay' # <1> html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table) # <2> puts html # <3> <4> exit 0 # <5><6> ---- <1> Load library <2> Highlight source <3> Print to stdout <4> Redirect to a file to capture output <5> Exit program <6> Reports success EOS output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE assert_match(/coderay<\/span>.* \(1\)<\/b>$/, output) assert_match(/puts 'Hello, world!'<\/span>.* \(2\)<\/b>$/, output) assert_match(/puts html * \(3\)<\/b> \(4\)<\/b>$/, output) assert_match(/exit.* \(5\)<\/b> \(6\)<\/b><\/code>/, output) end test 'should restore callout marks to correct lines if source highlighter is coderay and table line numbering is enabled' do input = <<-EOS :source-highlighter: coderay :coderay-linenums-mode: table [source, ruby, numbered] ---- require 'coderay' # <1> html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table) # <2> puts html # <3> <4> exit 0 # <5><6> ---- <1> Load library <2> Highlight source <3> Print to stdout <4> Redirect to a file to capture output <5> Exit program <6> Reports success EOS output = render_embedded_string input, :safe => Asciidoctor::SafeMode::SAFE assert_match(/coderay<\/span>.* \(1\)<\/b>$/, output) assert_match(/puts 'Hello, world!'<\/span>.* \(2\)<\/b>$/, output) assert_match(/puts html * \(3\)<\/b> \(4\)<\/b>$/, output) assert_match(/exit.* \(5\)<\/b> \(6\)<\/b><\/pre>/, output) end test 'should link to CodeRay stylesheet if source-highlighter is coderay and linkcss is set' do input = <<-EOS :source-highlighter: coderay [source, ruby] ---- require 'coderay' html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table) ---- EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'linkcss' => ''} assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1 assert_css 'link[rel="stylesheet"][href="./asciidoctor-coderay.css"]', output, 1 end test 'should highlight source inline if source-highlighter attribute is coderay and coderay-css is style' do input = <<-EOS :source-highlighter: coderay :coderay-css: style [source, ruby] ---- require 'coderay' html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table) ---- EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :linkcss_default => true assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@style = "color:#036;font-weight:bold"][text() = "CodeRay"]', output, 1 assert_no_match(/\.CodeRay \{/, output) end test 'should include remote highlight.js assets if source-highlighter attribute is highlightjs' do input = <<-EOS :source-highlighter: highlightjs [source, javascript] ---- ---- EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE assert_match(/ {'source-highlighter' => 'html-pipeline'} assert_css 'pre[lang="ruby"]', output, 1 assert_css 'pre[lang="ruby"] > code', output, 1 assert_css 'pre[class]', output, 0 assert_css 'code[class]', output, 0 end test 'document cannot turn on source highlighting if safe mode is at least SERVER' do input = <<-EOS :source-highlighter: coderay EOS doc = document_from_string input, :safe => Asciidoctor::SafeMode::SERVER assert doc.attributes['source-highlighter'].nil? end end context 'Abstract and Part Intro' do test 'should make abstract on open block without title a quote block for article' do input = <<-EOS = Article [abstract] -- This article is about stuff. And other stuff. -- EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock.abstract', output, 1 assert_css '#preamble .quoteblock', output, 1 assert_css '.quoteblock > blockquote', output, 1 assert_css '.quoteblock > blockquote > .paragraph', output, 2 end test 'should make abstract on open block with title a quote block with title for article' do input = <<-EOS = Article .My abstract [abstract] -- This article is about stuff. -- EOS output = render_string input assert_css '.quoteblock', output, 1 assert_css '.quoteblock.abstract', output, 1 assert_css '#preamble .quoteblock', output, 1 assert_css '.quoteblock > .title', output, 1 assert_css '.quoteblock > .title + blockquote', output, 1 assert_css '.quoteblock > .title + blockquote > .paragraph', output, 1 end test 'should allow abstract in document with title if doctype is book' do input = <<-EOS = Book :doctype: book [abstract] Abstract for book with title is valid EOS output = render_string input assert_css '.abstract', output, 1 end test 'should not allow abstract as direct child of document if doctype is book' do input = <<-EOS :doctype: book [abstract] Abstract for book without title is invalid. EOS output = render_string input assert_css '.abstract', output, 0 end test 'should make abstract on open block without title rendered to DocBook' do input = <<-EOS = Article [abstract] -- This article is about stuff. And other stuff. -- EOS output = render_string input, :backend => 'docbook' assert_css 'abstract', output, 1 assert_css 'abstract > simpara', output, 2 end test 'should make abstract on open block with title rendered to DocBook' do input = <<-EOS = Article .My abstract [abstract] -- This article is about stuff. -- EOS output = render_string input, :backend => 'docbook' assert_css 'abstract', output, 1 assert_css 'abstract > title', output, 1 assert_css 'abstract > title + simpara', output, 1 end test 'should allow abstract in document with title if doctype is book rendered to DocBook' do input = <<-EOS = Book :doctype: book [abstract] Abstract for book with title is valid EOS output = render_string input, :backend => 'docbook' assert_css 'abstract', output, 1 end test 'should not allow abstract as direct child of document if doctype is book rendered to DocBook' do input = <<-EOS :doctype: book [abstract] Abstract for book is invalid. EOS output = render_string input, :backend => 'docbook' assert_css 'abstract', output, 0 end # TODO partintro shouldn't be recognized if doctype is not book, should be in proper place test 'should accept partintro on open block without title' do input = <<-EOS = Book :doctype: book = Part 1 [partintro] -- This is a part intro. It can have multiple paragraphs. -- EOS output = render_string input assert_css '.openblock', output, 1 assert_css '.openblock.partintro', output, 1 assert_css '.openblock .title', output, 0 assert_css '.openblock .content', output, 1 assert_xpath %(//h1[@id="_part_1"]/following-sibling::*[#{contains_class(:openblock)}]), output, 1 assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="content"]/*[@class="paragraph"]), output, 2 end test 'should accept partintro on open block with title' do input = <<-EOS = Book :doctype: book = Part 1 .Intro title [partintro] -- This is a part intro with a title. -- EOS output = render_string input assert_css '.openblock', output, 1 assert_css '.openblock.partintro', output, 1 assert_css '.openblock .title', output, 1 assert_css '.openblock .content', output, 1 assert_xpath %(//h1[@id="_part_1"]/following-sibling::*[#{contains_class(:openblock)}]), output, 1 assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="title"][text() = "Intro title"]), output, 1 assert_xpath %(//*[#{contains_class(:openblock)}]/*[@class="content"]/*[@class="paragraph"]), output, 1 end test 'should exclude partintro if not a child of part' do input = <<-EOS = Book :doctype: book [partintro] part intro paragraph EOS output = render_string input assert_css '.partintro', output, 0 end test 'should not allow partintro unless doctype is book' do input = <<-EOS [partintro] part intro paragraph EOS output = render_string input assert_css '.partintro', output, 0 end test 'should accept partintro on open block without title rendered to DocBook' do input = <<-EOS = Book :doctype: book = Part 1 [partintro] -- This is a part intro. It can have multiple paragraphs. -- EOS output = render_string input, :backend => 'docbook' assert_css 'partintro', output, 1 assert_css 'part#_part_1 > partintro', output, 1 assert_css 'partintro > simpara', output, 2 end test 'should accept partintro on open block with title rendered to DocBook' do input = <<-EOS = Book :doctype: book = Part 1 .Intro title [partintro] -- This is a part intro with a title. -- EOS output = render_string input, :backend => 'docbook' assert_css 'partintro', output, 1 assert_css 'part#_part_1 > partintro', output, 1 assert_css 'partintro > title', output, 1 assert_css 'partintro > title + simpara', output, 1 end test 'should exclude partintro if not a child of part rendered to DocBook' do input = <<-EOS = Book :doctype: book [partintro] part intro paragraph EOS output = render_string input, :backend => 'docbook' assert_css 'partintro', output, 0 end test 'should not allow partintro unless doctype is book rendered to DocBook' do input = <<-EOS [partintro] part intro paragraph EOS output = render_string input, :backend => 'docbook' assert_css 'partintro', output, 0 end end end asciidoctor-0.1.4/test/document_test.rb000066400000000000000000001503111221220517700202000ustar00rootroot00000000000000require 'test_helper' context 'Document' do context 'Example document' do test 'document title' do doc = example_document(:asciidoc_index) assert_equal 'AsciiDoc Home Page', doc.doctitle assert_equal 'AsciiDoc Home Page', doc.name assert_equal 14, doc.blocks.size assert_equal :preamble, doc.blocks[0].context assert doc.blocks[1].is_a? ::Asciidoctor::Section end end context 'Default settings' do test 'safe mode level set to SECURE by default' do doc = Asciidoctor::Document.new assert_equal Asciidoctor::SafeMode::SECURE, doc.safe end test 'safe mode level set using string' do doc = Asciidoctor::Document.new [], :safe => 'server' assert_equal Asciidoctor::SafeMode::SERVER, doc.safe doc = Asciidoctor::Document.new [], :safe => 'foo' assert_equal Asciidoctor::SafeMode::SECURE, doc.safe end test 'safe mode level set using symbol' do doc = Asciidoctor::Document.new [], :safe => :server assert_equal Asciidoctor::SafeMode::SERVER, doc.safe doc = Asciidoctor::Document.new [], :safe => :foo assert_equal Asciidoctor::SafeMode::SECURE, doc.safe end test 'safe mode level set using integer' do doc = Asciidoctor::Document.new [], :safe => 10 assert_equal Asciidoctor::SafeMode::SERVER, doc.safe doc = Asciidoctor::Document.new [], :safe => 100 assert_equal 100, doc.safe end test 'safe mode attributes are set on document' do doc = Asciidoctor::Document.new assert_equal Asciidoctor::SafeMode::SECURE, doc.attr('safe-mode-level') assert_equal 'secure', doc.attr('safe-mode-name') assert doc.attr?('safe-mode-secure') assert !doc.attr?('safe-mode-unsafe') assert !doc.attr?('safe-mode-safe') assert !doc.attr?('safe-mode-server') end test 'safe mode level can be set in the constructor' do doc = Asciidoctor::Document.new [], :safe => Asciidoctor::SafeMode::SAFE assert_equal Asciidoctor::SafeMode::SAFE, doc.safe end test 'safe model level cannot be modified' do doc = Asciidoctor::Document.new begin doc.safe = Asciidoctor::SafeMode::UNSAFE flunk 'safe mode property of Asciidoctor::Document should not be writable!' rescue end end test 'toc and numbered should be enabled by default for DocBook backend' do doc = Asciidoctor::Document.new [], :backend => 'docbook' assert doc.attr?('toc') assert doc.attr?('numbered') end test 'should be able to disable toc and numbered in document header for DocBook backend' do input = <<-EOS = Document Title :toc!: :numbered!: EOS doc = document_from_string input, :backend => 'docbook' assert !doc.attr?('toc') assert !doc.attr?('numbered') end end context 'Load APIs' do test 'should load input file' do sample_input_path = fixture_path('sample.asciidoc') doc = Asciidoctor.load(File.new(sample_input_path), :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'Document Title', doc.doctitle assert_equal File.expand_path(sample_input_path), doc.attr('docfile') assert_equal File.expand_path(File.dirname(sample_input_path)), doc.attr('docdir') end test 'should load input file from filename' do sample_input_path = fixture_path('sample.asciidoc') doc = Asciidoctor.load_file(sample_input_path, :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'Document Title', doc.doctitle assert_equal File.expand_path(sample_input_path), doc.attr('docfile') assert_equal File.expand_path(File.dirname(sample_input_path)), doc.attr('docdir') end test 'should load input IO' do input = StringIO.new(<<-EOS) Document Title ============== preamble EOS doc = Asciidoctor.load(input, :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'Document Title', doc.doctitle assert !doc.attr?('docfile') assert_equal doc.base_dir, doc.attr('docdir') end test 'should load input string' do input = <<-EOS Document Title ============== preamble EOS doc = Asciidoctor.load(input, :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'Document Title', doc.doctitle assert !doc.attr?('docfile') assert_equal doc.base_dir, doc.attr('docdir') end test 'should load input string array' do input = <<-EOS Document Title ============== preamble EOS doc = Asciidoctor.load(input.lines.entries, :safe => Asciidoctor::SafeMode::SAFE) assert_equal 'Document Title', doc.doctitle assert !doc.attr?('docfile') assert_equal doc.base_dir, doc.attr('docdir') end test 'should accept attributes as array' do # NOTE there's a tab character before idseparator doc = Asciidoctor.load('text', :attributes => %w(toc numbered source-highlighter=coderay idprefix idseparator=-)) assert doc.attributes.is_a?(Hash) assert doc.attr?('toc') assert_equal '', doc.attr('toc') assert doc.attr?('numbered') assert_equal '', doc.attr('numbered') assert doc.attr?('source-highlighter') assert_equal 'coderay', doc.attr('source-highlighter') assert doc.attr?('idprefix') assert_equal '', doc.attr('idprefix') assert doc.attr?('idseparator') assert_equal '-', doc.attr('idseparator') end test 'should accept attributes as empty array' do doc = Asciidoctor.load('text', :attributes => []) assert doc.attributes.is_a?(Hash) end test 'should accept attributes as string' do # NOTE there's a tab character before idseparator doc = Asciidoctor.load('text', :attributes => 'toc numbered source-highlighter=coderay idprefix idseparator=-') assert doc.attributes.is_a?(Hash) assert doc.attr?('toc') assert_equal '', doc.attr('toc') assert doc.attr?('numbered') assert_equal '', doc.attr('numbered') assert doc.attr?('source-highlighter') assert_equal 'coderay', doc.attr('source-highlighter') assert doc.attr?('idprefix') assert_equal '', doc.attr('idprefix') assert doc.attr?('idseparator') assert_equal '-', doc.attr('idseparator') end test 'should accept values containing spaces in attributes string' do # NOTE there's a tab character before self: doc = Asciidoctor.load('text', :attributes => 'idprefix idseparator=- note-caption=Note\ to\ self: toc') assert doc.attributes.is_a?(Hash) assert doc.attr?('idprefix') assert_equal '', doc.attr('idprefix') assert doc.attr?('idseparator') assert_equal '-', doc.attr('idseparator') assert doc.attr?('note-caption') assert_equal "Note to self:", doc.attr('note-caption') end test 'should accept attributes as empty string' do doc = Asciidoctor.load('text', :attributes => '') assert doc.attributes.is_a?(Hash) end test 'should accept attributes as nil' do doc = Asciidoctor.load('text', :attributes => nil) assert doc.attributes.is_a?(Hash) end test 'should accept attributes if hash like' do class Hashish def initialize @table = {'toc' => ''} end def keys @table.keys end def [](key) @table[key] end end doc = Asciidoctor.load('text', :attributes => Hashish.new) assert doc.attributes.is_a?(Hash) assert doc.attributes.has_key?('toc') end end context 'Render APIs' do test 'should render document to string' do sample_input_path = fixture_path('sample.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true) assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1 end test 'should accept attributes as array' do sample_input_path = fixture_path('sample.asciidoc') output = Asciidoctor.render_file(sample_input_path, :attributes => %w(numbered idprefix idseparator=-)) assert_css '#section-a', output, 1 end test 'should accept attributes as string' do sample_input_path = fixture_path('sample.asciidoc') output = Asciidoctor.render_file(sample_input_path, :attributes => 'numbered idprefix idseparator=-') assert_css '#section-a', output, 1 end test 'should link to default stylesheet by default when safe mode is SECURE or greater' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true) assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1 end test 'should embed default stylesheet by default if SafeMode is less than SECURE' do input = <<-EOS = Document Title text EOS output = Asciidoctor.render(input, :safe => Asciidoctor::SafeMode::SERVER, :header_footer => true) assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 0 stylenode = xmlnodes_at_css 'html:root > head > style', output, 1 styles = stylenode.first.content assert !styles.nil? assert !styles.strip.empty? end test 'should link to default stylesheet by default if linkcss is unset in document' do input = <<-EOS = Document Title :linkcss!: text EOS output = Asciidoctor.render(input, :header_footer => true) assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1 end test 'should link to default stylesheet by default if linkcss is unset' do input = <<-EOS = Document Title text EOS output = Asciidoctor.render(input, :header_footer => true, :attributes => {'linkcss!' => ''}) assert_css 'html:root > head > link[rel="stylesheet"][href="./asciidoctor.css"]', output, 1 end test 'should embed default stylesheet if safe mode is less than secure and linkcss is unset' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'linkcss!' => ''}) assert_css 'html:root > head > style', output, 1 stylenode = xmlnodes_at_css 'html:root > head > style', output, 1 styles = stylenode.first.content assert !styles.nil? assert !styles.strip.empty? end test 'should not link to stylesheet if stylesheet is unset' do input = <<-EOS = Document Title text EOS output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet!' => ''}) assert_css 'html:root > head > link[rel="stylesheet"]', output, 0 end test 'should link to custom stylesheet if specified in stylesheet attribute' do input = <<-EOS = Document Title text EOS output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet' => './custom.css'}) assert_css 'html:root > head > link[rel="stylesheet"][href="./custom.css"]', output, 1 end test 'should resolve custom stylesheet relative to stylesdir' do input = <<-EOS = Document Title text EOS output = Asciidoctor.render(input, :header_footer => true, :attributes => {'stylesheet' => 'custom.css', 'stylesdir' => './stylesheets'}) assert_css 'html:root > head > link[rel="stylesheet"][href="./stylesheets/custom.css"]', output, 1 end test 'should resolve custom stylesheet to embed relative to stylesdir' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'stylesheet' => 'custom.css', 'stylesdir' => './stylesheets', 'linkcss!' => ''}) stylenode = xmlnodes_at_css 'html:root > head > style', output, 1 styles = stylenode.first.content assert !styles.nil? assert !styles.strip.empty? end test 'should render document in place' do sample_input_path = fixture_path('sample.asciidoc') sample_output_path = fixture_path('sample.html') begin Asciidoctor.render_file(sample_input_path, :in_place => true) assert File.exist?(sample_output_path) output = File.read(sample_output_path) assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1 ensure FileUtils::rm(sample_output_path) end end test 'should render document to file' do sample_input_path = fixture_path('sample.asciidoc') sample_output_path = fixture_path('result.html') begin Asciidoctor.render_file(sample_input_path, :to_file => sample_output_path) assert File.exist?(sample_output_path) output = File.read(sample_output_path) assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1 ensure FileUtils::rm(sample_output_path) end end test 'should render document to file when base dir is set' do sample_input_path = fixture_path('sample.asciidoc') sample_output_path = fixture_path('result.html') fixture_dir = fixture_path('') begin Asciidoctor.render_file(sample_input_path, :to_file => 'result.html', :base_dir => fixture_dir) assert File.exist?(sample_output_path) output = File.read(sample_output_path) assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1 rescue => e flunk e.message ensure FileUtils::rm(sample_output_path, :force => true) end end test 'in_place option must not be used with to_file option' do sample_input_path = fixture_path('sample.asciidoc') sample_output_path = fixture_path('result.html') assert_raise ArgumentError do begin Asciidoctor.render_file(sample_input_path, :to_file => sample_output_path, :in_place => true) ensure FileUtils::rm(sample_output_path) if File.exists? sample_output_path end end end test 'in_place option must not be used with to_dir option' do sample_input_path = fixture_path('sample.asciidoc') sample_output_path = fixture_path('result.html') assert_raise ArgumentError do begin Asciidoctor.render_file(sample_input_path, :to_dir => '', :in_place => true) ensure FileUtils::rm(sample_output_path) if File.exists? sample_output_path end end end test 'output should be relative to to_dir option' do sample_input_path = fixture_path('sample.asciidoc') output_dir = File.join(File.dirname(sample_input_path), 'test_output') Dir.mkdir output_dir if !File.exists? output_dir sample_output_path = File.join(output_dir, 'sample.html') begin Asciidoctor.render_file(sample_input_path, :to_dir => output_dir) assert File.exists? sample_output_path ensure FileUtils::rm(sample_output_path) if File.exists? sample_output_path FileUtils::rmdir output_dir end end test 'missing directories should be created if mkdirs is enabled' do sample_input_path = fixture_path('sample.asciidoc') output_dir = File.join(File.join(File.dirname(sample_input_path), 'test_output'), 'subdir') sample_output_path = File.join(output_dir, 'sample.html') begin Asciidoctor.render_file(sample_input_path, :to_dir => output_dir, :mkdirs => true) assert File.exists? sample_output_path ensure FileUtils::rm(sample_output_path) if File.exists? sample_output_path FileUtils::rmdir output_dir FileUtils::rmdir File.dirname(output_dir) end end test 'to_file should be relative to to_dir when both given' do sample_input_path = fixture_path('sample.asciidoc') base_dir = File.dirname(sample_input_path) sample_rel_output_path = File.join('test_output', 'result.html') output_dir = File.dirname(File.join(base_dir, sample_rel_output_path)) Dir.mkdir output_dir if !File.exists? output_dir sample_output_path = File.join(base_dir, sample_rel_output_path) begin Asciidoctor.render_file(sample_input_path, :to_dir => base_dir, :to_file => sample_rel_output_path) assert File.exists? sample_output_path ensure FileUtils::rm(sample_output_path) if File.exists? sample_output_path FileUtils::rmdir output_dir end end end context 'Docinfo files' do test 'should include docinfo files for html backend' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''}) assert !output.empty? assert_css 'script[src="modernizr.js"]', output, 1 assert_css 'meta[http-equiv="imagetoolbar"]', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''}) assert !output.empty? assert_css 'script[src="modernizr.js"]', output, 0 assert_css 'meta[http-equiv="imagetoolbar"]', output, 1 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'script[src="modernizr.js"]', output, 1 assert_css 'meta[http-equiv="imagetoolbar"]', output, 1 end test 'should include docinfo files for docbook backend' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''}) assert !output.empty? assert_css 'productname', output, 0 assert_css 'copyright', output, 1 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''}) assert !output.empty? assert_css 'productname', output, 1 assert_css 'edition', output, 1 assert_xpath '//xmlns:edition[text()="1.0"]', output, 1 # verifies substitutions are performed assert_css 'copyright', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'productname', output, 1 assert_css 'edition', output, 1 assert_xpath '//xmlns:edition[text()="1.0"]', output, 1 # verifies substitutions are performed assert_css 'copyright', output, 1 end test 'should include docinfo footer files for html backend' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''}) assert !output.empty? assert_css 'body script', output, 1 assert_css 'a#top', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''}) assert !output.empty? assert_css 'body script', output, 0 assert_css 'a#top', output, 1 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'body script', output, 1 assert_css 'a#top', output, 1 end test 'should include docinfo footer files for docbook backend' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo' => ''}) assert !output.empty? assert_css 'article > revhistory', output, 1 assert_xpath '/xmlns:article/xmlns:revhistory/xmlns:revision/xmlns:revnumber[text()="1.0"]', output, 1 # verifies substitutions are performed assert_css 'glossary#_glossary', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo1' => ''}) assert !output.empty? assert_css 'article > revhistory', output, 0 assert_css 'glossary#_glossary', output, 1 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER, :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'article > revhistory', output, 1 assert_xpath '/xmlns:article/xmlns:revhistory/xmlns:revision/xmlns:revnumber[text()="1.0"]', output, 1 # verifies substitutions are performed assert_css 'glossary#_glossary', output, 1 end test 'should not include docinfo files by default' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :safe => Asciidoctor::SafeMode::SERVER) assert !output.empty? assert_css 'script[src="modernizr.js"]', output, 0 assert_css 'meta[http-equiv="imagetoolbar"]', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :safe => Asciidoctor::SafeMode::SERVER) assert !output.empty? assert_css 'productname', output, 0 assert_css 'copyright', output, 0 end test 'should not include docinfo files if safe mode is SECURE or greater' do sample_input_path = fixture_path('basic.asciidoc') output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'script[src="modernizr.js"]', output, 0 assert_css 'meta[http-equiv="imagetoolbar"]', output, 0 output = Asciidoctor.render_file(sample_input_path, :header_footer => true, :backend => 'docbook', :attributes => {'docinfo2' => ''}) assert !output.empty? assert_css 'productname', output, 0 assert_css 'copyright', output, 0 end end context 'Renderer' do test 'built-in HTML5 views are registered by default' do doc = document_from_string '' assert_equal 'html5', doc.attributes['backend'] assert doc.attributes.has_key? 'backend-html5' assert_equal 'html', doc.attributes['basebackend'] assert doc.attributes.has_key? 'basebackend-html' renderer = doc.renderer assert !renderer.nil? views = renderer.views assert !views.nil? assert_equal 36, views.size assert views.has_key? 'document' assert Asciidoctor.const_defined?(:HTML5) assert Asciidoctor::HTML5.const_defined?(:DocumentTemplate) end test 'built-in DocBook45 views are registered when backend is docbook45' do doc = document_from_string '', :attributes => {'backend' => 'docbook45'} renderer = doc.renderer assert_equal 'docbook45', doc.attributes['backend'] assert doc.attributes.has_key? 'backend-docbook45' assert_equal 'docbook', doc.attributes['basebackend'] assert doc.attributes.has_key? 'basebackend-docbook' assert !renderer.nil? views = renderer.views assert !views.nil? assert_equal 36, views.size assert views.has_key? 'document' assert Asciidoctor.const_defined?(:DocBook45) assert Asciidoctor::DocBook45.const_defined?(:DocumentTemplate) end test 'built-in DocBook5 views are registered when backend is docbook5' do doc = document_from_string '', :attributes => {'backend' => 'docbook5'} renderer = doc.renderer assert_equal 'docbook5', doc.attributes['backend'] assert doc.attributes.has_key? 'backend-docbook5' assert_equal 'docbook', doc.attributes['basebackend'] assert doc.attributes.has_key? 'basebackend-docbook' assert !renderer.nil? views = renderer.views assert !views.nil? assert_equal 36, views.size assert views.has_key? 'document' assert Asciidoctor.const_defined?(:DocBook5) assert Asciidoctor::DocBook5.const_defined?(:DocumentTemplate) end test 'eRuby implementation should default to ERB' do # intentionally use built-in templates for this test doc = Asciidoctor::Document.new [], :header_footer => true renderer = doc.renderer views = renderer.views assert !views.nil? assert views.has_key? 'document' assert views['document'].is_a?(Asciidoctor::HTML5::DocumentTemplate) assert_equal 'ERB', views['document'].eruby.to_s assert_equal 'ERB', views['document'].template.class.to_s end test 'can set erubis as eRuby implementation' do # intentionally use built-in templates for this test doc = Asciidoctor::Document.new [], :eruby => 'erubis', :header_footer => true assert $LOADED_FEATURES.detect {|p| p == 'erubis.rb' || p.end_with?('/erubis.rb') }.nil? renderer = doc.renderer assert $LOADED_FEATURES.detect {|p| p == 'erubis.rb' || p.end_with?('/erubis.rb') } views = renderer.views assert !views.nil? assert views.has_key? 'document' assert views['document'].is_a?(Asciidoctor::HTML5::DocumentTemplate) assert_equal 'Erubis::FastEruby', views['document'].eruby.to_s assert_equal 'Erubis::FastEruby', views['document'].template.class.to_s end end context 'Structure' do test 'document with no doctitle' do doc = document_from_string('Snorf') assert_nil doc.doctitle assert_nil doc.name assert !doc.has_header? assert_nil doc.header end test 'document with doctitle defined as attribute entry' do input = <<-EOS :doctitle: Document Title preamble == First Section EOS doc = document_from_string input assert_equal 'Document Title', doc.doctitle assert doc.has_header? assert_equal 'Document Title', doc.header.title assert_equal 'Document Title', doc.first_section.title end test 'document with title attribute entry overrides doctitle' do input = <<-EOS = Document Title :title: Override {doctitle} == First Section EOS doc = document_from_string input assert_equal 'Override', doc.doctitle assert_equal 'Override', doc.title assert doc.has_header? assert_equal 'Document Title', doc.header.title assert_equal 'Document Title', doc.first_section.title assert_xpath '//*[@id="preamble"]//p[text()="Document Title"]', doc.render, 1 end test 'document with title attribute entry overrides doctitle attribute entry' do input = <<-EOS = Document Title :snapshot: {doctitle} :doctitle: doctitle :title: Override {snapshot}, {doctitle} == First Section EOS doc = document_from_string input assert_equal 'Override', doc.doctitle assert_equal 'Override', doc.title assert doc.has_header? assert_equal 'doctitle', doc.header.title assert_equal 'doctitle', doc.first_section.title assert_xpath '//*[@id="preamble"]//p[text()="Document Title, doctitle"]', doc.render, 1 end test 'document with doctitle attribute entry overrides header title and doctitle' do input = <<-EOS = Document Title :snapshot: {doctitle} :doctitle: Override {snapshot}, {doctitle} == First Section EOS doc = document_from_string input assert_equal 'Override', doc.doctitle assert_nil doc.title assert doc.has_header? assert_equal 'Override', doc.header.title assert_equal 'Override', doc.first_section.title assert_xpath '//*[@id="preamble"]//p[text()="Document Title, Override"]', doc.render, 1 end test 'doctitle attribute entry above header overrides header title and doctitle' do input = <<-EOS :doctitle: Override = Document Title {doctitle} == First Section EOS doc = document_from_string input assert_equal 'Override', doc.doctitle assert_nil doc.title assert doc.has_header? assert_equal 'Override', doc.header.title assert_equal 'Override', doc.first_section.title assert_xpath '//*[@id="preamble"]//p[text()="Override"]', doc.render, 1 end test 'should recognize document title when preceded by blank lines' do input = <<-EOS :doctype: book = Title preamble == Section 1 text EOS output = render_string input, :safe => Asciidoctor::SafeMode::SAFE assert_css '#header h1', output, 1 assert_css '#content h1', output, 0 end test 'should sanitize contents of HTML title element' do input = <<-EOS = *Document* image:logo.png[] _Title_ image:another-logo.png[] content EOS output = render_string input assert_xpath '/html/head/title[text()="Document Title"]', output, 1 nodes = xmlnodes_at_xpath('//*[@id="header"]/h1', output, 1) assert_equal 1, nodes.size assert_match(/

    Document<\/strong> logo<\/span> Title<\/em> another-logo<\/span><\/h1>/, output) end test 'should not choke on empty source' do doc = Asciidoctor::Document.new '' assert doc.blocks.empty? assert_nil doc.doctitle assert !doc.has_header? assert_nil doc.header end test 'should not choke on nil source' do doc = Asciidoctor::Document.new nil assert doc.blocks.empty? assert_nil doc.doctitle assert !doc.has_header? assert_nil doc.header end test 'with metadata' do input = <<-EOS = AsciiDoc Stuart Rackham v8.6.8, 2012-07-12: See changelog. == Version 8.6.8 more info... EOS output = render_string input assert_xpath '//*[@id="header"]/span[@id="author"][text() = "Stuart Rackham"]', output, 1 assert_xpath '//*[@id="header"]/span[@id="email"]/a[@href="mailto:founder@asciidoc.org"][text() = "founder@asciidoc.org"]', output, 1 assert_xpath '//*[@id="header"]/span[@id="revnumber"][text() = "version 8.6.8,"]', output, 1 assert_xpath '//*[@id="header"]/span[@id="revdate"][text() = "2012-07-12"]', output, 1 assert_xpath '//*[@id="header"]/span[@id="revremark"][text() = "See changelog."]', output, 1 end test 'with metadata to DocBook45' do input = <<-EOS = AsciiDoc Stuart Rackham v8.6.8, 2012-07-12: See changelog. == Version 8.6.8 more info... EOS output = render_string input, :backend => 'docbook' assert_xpath '/article/articleinfo', output, 1 assert_xpath '/article/articleinfo/title[text() = "AsciiDoc"]', output, 1 assert_xpath '/article/articleinfo/date[text() = "2012-07-12"]', output, 1 assert_xpath '/article/articleinfo/author/firstname[text() = "Stuart"]', output, 1 assert_xpath '/article/articleinfo/author/surname[text() = "Rackham"]', output, 1 assert_xpath '/article/articleinfo/author/email[text() = "founder@asciidoc.org"]', output, 1 assert_xpath '/article/articleinfo/revhistory', output, 1 assert_xpath '/article/articleinfo/revhistory/revision', output, 1 assert_xpath '/article/articleinfo/revhistory/revision/revnumber[text() = "8.6.8"]', output, 1 assert_xpath '/article/articleinfo/revhistory/revision/date[text() = "2012-07-12"]', output, 1 assert_xpath '/article/articleinfo/revhistory/revision/authorinitials[text() = "SR"]', output, 1 assert_xpath '/article/articleinfo/revhistory/revision/revremark[text() = "See changelog."]', output, 1 end test 'with metadata to DocBook5' do input = <<-EOS = AsciiDoc Stuart Rackham == Version 8.6.8 more info... EOS output = render_string input, :backend => 'docbook5' assert_xpath '/article/info', output, 1 assert_xpath '/article/info/title[text() = "AsciiDoc"]', output, 1 assert_xpath '/article/info/author/personname', output, 1 assert_xpath '/article/info/author/personname/firstname[text() = "Stuart"]', output, 1 assert_xpath '/article/info/author/personname/surname[text() = "Rackham"]', output, 1 assert_xpath '/article/info/author/email[text() = "founder@asciidoc.org"]', output, 1 end test 'with author defined using attribute entry to DocBook' do input = <<-EOS = Document Title :author: Doc Writer :email: thedoctor@asciidoc.org content EOS output = render_string input, :backend => 'docbook' assert_xpath '//articleinfo/author', output, 1 assert_xpath '//articleinfo/author/firstname[text() = "Doc"]', output, 1 assert_xpath '//articleinfo/author/surname[text() = "Writer"]', output, 1 assert_xpath '//articleinfo/author/email[text() = "thedoctor@asciidoc.org"]', output, 1 assert_xpath '//articleinfo/authorinitials[text() = "DW"]', output, 1 end test 'should include multiple authors in HTML output' do input = <<-EOS = Document Title Doc Writer ; Junior Writer content EOS output = render_string input assert_xpath '//span[@id="author"]', output, 1 assert_xpath '//span[@id="author"][text()="Doc Writer"]', output, 1 assert_xpath '//span[@id="email"]', output, 1 assert_xpath '//span[@id="email"]/a', output, 1 assert_xpath '//span[@id="email"]/a[@href="mailto:thedoctor@asciidoc.org"][text()="thedoctor@asciidoc.org"]', output, 1 assert_xpath '//span[@id="author2"]', output, 1 assert_xpath '//span[@id="author2"][text()="Junior Writer"]', output, 1 assert_xpath '//span[@id="email2"]', output, 1 assert_xpath '//span[@id="email2"]/a', output, 1 assert_xpath '//span[@id="email2"]/a[@href="mailto:junior@asciidoctor.org"][text()="junior@asciidoctor.org"]', output, 1 end test 'should create authorgroup in DocBook when multiple authors' do input = <<-EOS = Document Title Doc Writer ; Junior Writer content EOS output = render_string input, :backend => 'docbook' assert_xpath '//articleinfo/author', output, 0 assert_xpath '//articleinfo/authorgroup', output, 1 assert_xpath '//articleinfo/authorgroup/author', output, 2 assert_xpath '//articleinfo/authorgroup/author[1]/firstname[text() = "Doc"]', output, 1 assert_xpath '//articleinfo/authorgroup/author[2]/firstname[text() = "Junior"]', output, 1 end test 'with authors defined using attribute entry to DocBook' do input = <<-EOS = Document Title :authors: Doc Writer; Junior Writer :email_1: thedoctor@asciidoc.org :email_2: junior@asciidoc.org content EOS output = render_string input, :backend => 'docbook' assert_xpath '//articleinfo/author', output, 0 assert_xpath '//articleinfo/authorgroup', output, 1 assert_xpath '//articleinfo/authorgroup/author', output, 2 assert_xpath '(//articleinfo/authorgroup/author)[1]/firstname[text() = "Doc"]', output, 1 assert_xpath '(//articleinfo/authorgroup/author)[1]/email[text() = "thedoctor@asciidoc.org"]', output, 1 assert_xpath '(//articleinfo/authorgroup/author)[2]/firstname[text() = "Junior"]', output, 1 assert_xpath '(//articleinfo/authorgroup/author)[2]/email[text() = "junior@asciidoc.org"]', output, 1 end test 'with header footer' do doc = document_from_string "= Title\n\npreamble" assert !doc.attr?('embedded') result = doc.render assert_xpath '/html', result, 1 assert_xpath '//*[@id="header"]', result, 1 assert_xpath '//*[@id="header"]/h1', result, 1 assert_xpath '//*[@id="footer"]', result, 1 assert_xpath '//*[@id="preamble"]', result, 1 end test 'can disable last updated in footer' do doc = document_from_string "= Document Title\n\npreamble", :attributes => {'last-update-label!' => ''} result = doc.render assert_xpath '//*[@id="footer-text"]', result, 1 assert_xpath '//*[@id="footer-text"][normalize-space(text())=""]', result, 1 end test 'no header footer' do doc = document_from_string "= Title\n\npreamble", :header_footer => false assert doc.attr?('embedded') result = doc.render assert_xpath '/html', result, 0 assert_xpath '/h1', result, 0 assert_xpath '/*[@id="header"]', result, 0 assert_xpath '/*[@id="footer"]', result, 0 assert_xpath '/*[@id="preamble"]', result, 1 end test 'enable title in embedded document by unassigning notitle attribute' do input = <<-EOS = Document Title content EOS result = render_string input, :header_footer => false, :attributes => {'notitle!' => ''} assert_xpath '/html', result, 0 assert_xpath '/h1', result, 1 assert_xpath '/*[@id="header"]', result, 0 assert_xpath '/*[@id="footer"]', result, 0 assert_xpath '/*[@id="preamble"]', result, 1 assert_xpath '(/*)[1]/self::h1', result, 1 assert_xpath '(/*)[2]/self::*[@id="preamble"]', result, 1 end test 'enable title in embedded document by assigning showtitle attribute' do input = <<-EOS = Document Title content EOS result = render_string input, :header_footer => false, :attributes => {'showtitle' => ''} assert_xpath '/html', result, 0 assert_xpath '/h1', result, 1 assert_xpath '/*[@id="header"]', result, 0 assert_xpath '/*[@id="footer"]', result, 0 assert_xpath '/*[@id="preamble"]', result, 1 assert_xpath '(/*)[1]/self::h1', result, 1 assert_xpath '(/*)[2]/self::*[@id="preamble"]', result, 1 end test 'parse header only' do input = <<-EOS = Document Title Author Name :foo: bar preamble EOS doc = document_from_string input, :parse_header_only => true assert_equal 'Document Title', doc.doctitle assert_equal 'Author Name', doc.author assert_equal 'bar', doc.attributes['foo'] # there would be at least 1 block had it parsed beyond the header assert_equal 0, doc.blocks.size end test 'renders footnotes in footer' do input = <<-EOS A footnote footnote:[An example footnote.]; a second footnote with a reference ID footnoteref:[note2,Second footnote.]; finally a reference to the second footnote footnoteref:[note2]. EOS output = render_string input assert_css '#footnotes', output, 1 assert_css '#footnotes .footnote', output, 2 assert_css '#footnotes .footnote#_footnote_1', output, 1 assert_xpath '//div[@id="footnotes"]/div[@id="_footnote_1"]/a[@href="#_footnoteref_1"][text()="1"]', output, 1 text = xmlnodes_at_xpath '//div[@id="footnotes"]/div[@id="_footnote_1"]/text()', output, 1 assert_equal '. An example footnote.', text.text.strip assert_css '#footnotes .footnote#_footnote_2', output, 1 assert_xpath '//div[@id="footnotes"]/div[@id="_footnote_2"]/a[@href="#_footnoteref_2"][text()="2"]', output, 1 text = xmlnodes_at_xpath '//div[@id="footnotes"]/div[@id="_footnote_2"]/text()', output, 1 assert_equal '. Second footnote.', text.text.strip end test 'renders footnotes block in embedded document by default' do input = <<-EOS Text that has supporting information{empty}footnote:[An example footnote.]. EOS output = render_string input, :header_footer => false assert_css '#footnotes', output, 1 end test 'does not render footnotes block in embedded document if nofootnotes attribute is set' do input = <<-EOS Text that has supporting information{empty}footnote:[An example footnote.]. EOS output = render_string input, :header_footer => false, :attributes => {'nofootnotes' => ''} assert_css '#footnotes', output, 0 end end context 'Backends and Doctypes' do test 'html5 backend doctype article' do result = render_string("= Title\n\npreamble", :attributes => {'backend' => 'html5'}) assert_xpath '/html', result, 1 assert_xpath '/html/body[@class="article"]', result, 1 assert_xpath '/html//*[@id="header"]/h1[text() = "Title"]', result, 1 assert_xpath '/html//*[@id="preamble"]//p[text() = "preamble"]', result, 1 end test 'html5 backend doctype book' do result = render_string("= Title\n\npreamble", :attributes => {'backend' => 'html5', 'doctype' => 'book'}) assert_xpath '/html', result, 1 assert_xpath '/html/body[@class="book"]', result, 1 assert_xpath '/html//*[@id="header"]/h1[text() = "Title"]', result, 1 assert_xpath '/html//*[@id="preamble"]//p[text() = "preamble"]', result, 1 end test 'docbook45 backend doctype article' do input = <<-EOS = Title preamble == First Section section body EOS result = render_string(input, :attributes => {'backend' => 'docbook45'}) assert_xpath '/article', result, 1 assert_xpath '/article/articleinfo/title[text() = "Title"]', result, 1 assert_xpath '/article/simpara[text() = "preamble"]', result, 1 assert_xpath '/article/section', result, 1 assert_xpath '/article/section[@id = "_first_section"]/title[text() = "First Section"]', result, 1 assert_xpath '/article/section[@id = "_first_section"]/simpara[text() = "section body"]', result, 1 end test 'docbook45 backend doctype article no title' do result = render_string('text', :attributes => {'backend' => 'docbook45'}) assert_xpath '/article', result, 1 assert_xpath '/article/articleinfo/date', result, 1 assert_xpath '/article/simpara[text() = "text"]', result, 1 end test 'docbook45 backend doctype article no xmlns' do result = render_string('text', :keep_namespaces => true, :attributes => {'backend' => 'docbook45', 'doctype' => 'article', 'noxmlns' => ''}) assert_no_match(RE_XMLNS_ATTRIBUTE, result) end test 'docbook45 backend doctype book' do input = <<-EOS = Title preamble == First Chapter chapter body EOS result = render_string(input, :attributes => {'backend' => 'docbook45', 'doctype' => 'book'}) assert_xpath '/book', result, 1 assert_xpath '/book/bookinfo/title[text() = "Title"]', result, 1 assert_xpath '/book/preface/simpara[text() = "preamble"]', result, 1 assert_xpath '/book/chapter', result, 1 assert_xpath '/book/chapter[@id = "_first_chapter"]/title[text() = "First Chapter"]', result, 1 assert_xpath '/book/chapter[@id = "_first_chapter"]/simpara[text() = "chapter body"]', result, 1 end test 'docbook45 backend doctype book no title' do result = render_string('text', :attributes => {'backend' => 'docbook45', 'doctype' => 'book'}) assert_xpath '/book', result, 1 assert_xpath '/book/bookinfo/date', result, 1 assert_xpath '/book/simpara[text() = "text"]', result, 1 end test 'docbook45 backend doctype book no xmlns' do result = render_string('text', :keep_namespaces => true, :attributes => {'backend' => 'docbook45', 'doctype' => 'book', 'noxmlns' => ''}) assert_no_match(RE_XMLNS_ATTRIBUTE, result) end test 'docbook45 backend parses out subtitle' do input = <<-EOS = Document Title: Subtitle :doctype: book text EOS result = render_string input, :backend => 'docbook45' assert_xpath '/book', result, 1 assert_xpath '/book/bookinfo/title[text() = "Document Title"]', result, 1 assert_xpath '/book/bookinfo/subtitle[text() = "Subtitle"]', result, 1 end test 'docbook5 backend doctype article' do input = <<-EOS = Title Author Name preamble == First Section section body EOS result = render_string(input, :keep_namespaces => true, :attributes => {'backend' => 'docbook5'}) assert_xpath '/xmlns:article', result, 1 doc = xmlnodes_at_xpath('/xmlns:article', result, 1).first assert_equal 'http://docbook.org/ns/docbook', doc.namespaces['xmlns'] assert_equal 'http://www.w3.org/1999/xlink', doc.namespaces['xmlns:xlink'] assert_xpath '/xmlns:article[@version="5.0"]', result, 1 assert_xpath '/xmlns:article/xmlns:info/xmlns:title[text() = "Title"]', result, 1 assert_xpath '/xmlns:article/xmlns:simpara[text() = "preamble"]', result, 1 assert_xpath '/xmlns:article/xmlns:section', result, 1 section = xmlnodes_at_xpath('/xmlns:article/xmlns:section', result, 1).first # nokogiri can't make up its mind id_attr = section.attribute('id') || section.attribute('xml:id') assert_not_nil id_attr assert_not_nil id_attr.namespace assert_equal 'xml', id_attr.namespace.prefix assert_equal '_first_section', id_attr.value end test 'docbook5 backend doctype book' do input = <<-EOS = Title Author Name preamble == First Chapter chapter body EOS result = render_string(input, :keep_namespaces => true, :attributes => {'backend' => 'docbook5', 'doctype' => 'book'}) assert_xpath '/xmlns:book', result, 1 doc = xmlnodes_at_xpath('/xmlns:book', result, 1).first assert_equal 'http://docbook.org/ns/docbook', doc.namespaces['xmlns'] assert_equal 'http://www.w3.org/1999/xlink', doc.namespaces['xmlns:xlink'] assert_xpath '/xmlns:book[@version="5.0"]', result, 1 assert_xpath '/xmlns:book/xmlns:info/xmlns:title[text() = "Title"]', result, 1 assert_xpath '/xmlns:book/xmlns:preface/xmlns:simpara[text() = "preamble"]', result, 1 assert_xpath '/xmlns:book/xmlns:chapter', result, 1 chapter = xmlnodes_at_xpath('/xmlns:book/xmlns:chapter', result, 1).first # nokogiri can't make up its mind id_attr = chapter.attribute('id') || chapter.attribute('xml:id') assert_not_nil id_attr assert_not_nil id_attr.namespace assert_equal 'xml', id_attr.namespace.prefix assert_equal '_first_chapter', id_attr.value end test 'should be able to set backend using :backend option key' do doc = Asciidoctor::Document.new([], :backend => 'html5') assert_equal 'html5', doc.attributes['backend'] end test ':backend option should override backend attribute' do doc = Asciidoctor::Document.new([], :backend => 'html5', :attributes => {'backend' => 'docbook45'}) assert_equal 'html5', doc.attributes['backend'] end test 'should be able to set doctype using :doctype option key' do doc = Asciidoctor::Document.new([], :doctype => 'book') assert_equal 'book', doc.attributes['doctype'] end test ':doctype option should override doctype attribute' do doc = Asciidoctor::Document.new([], :doctype => 'book', :attributes => {'doctype' => 'article'}) assert_equal 'book', doc.attributes['doctype'] end test 'do not override explicit author initials' do input = <<-EOS = AsciiDoc Stuart Rackham :Author Initials: SJR more info... EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '/article/articleinfo/authorinitials[text()="SJR"]', output, 1 end test 'attribute entry can appear immediately after document title' do input = <<-EOS Reference Guide =============== :toc: preamble EOS doc = document_from_string input assert doc.attr?('toc') assert_equal '', doc.attr('toc') end test 'attribute entry can appear before author line under document title' do input = <<-EOS Reference Guide =============== :toc: Dan Allen preamble EOS doc = document_from_string input assert doc.attr?('toc') assert_equal '', doc.attr('toc') assert_equal 'Dan Allen', doc.attr('author') end test 'should parse mantitle and manvolnum from document title for manpage doctype' do input = <<-EOS = asciidoctor ( 1 ) :doctype: manpage == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats EOS doc = document_from_string input assert_equal 'asciidoctor', doc.attr('mantitle') assert_equal '1', doc.attr('manvolnum') end test 'should perform attribute substitution on mantitle in manpage doctype' do input = <<-EOS = {app}(1) :doctype: manpage :app: asciidoctor == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats EOS doc = document_from_string input assert_equal 'asciidoctor', doc.attr('mantitle') end test 'should consume name section as manname and manpurpose for manpage doctype' do input = <<-EOS = asciidoctor(1) :doctype: manpage == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats EOS doc = document_from_string input assert_equal 'asciidoctor', doc.attr('manname') assert_equal 'converts AsciiDoc source files to HTML, DocBook and other formats', doc.attr('manpurpose') assert_equal 0, doc.blocks.size end test 'should set docname and outfilesuffix from manname and manvolnum for manpage backend and doctype' do input = <<-EOS = asciidoctor(1) :doctype: manpage == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats EOS doc = document_from_string input, :backend => 'manpage' assert_equal 'asciidoctor', doc.attributes['docname'] assert_equal '.1', doc.attributes['outfilesuffix'] end test 'should mark synopsis as special section in manpage doctype' do input = <<-EOS = asciidoctor(1) :doctype: manpage == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats == SYNOPSIS *asciidoctor* ['OPTION']... 'FILE'.. EOS doc = document_from_string input synopsis_section = doc.blocks.first assert_not_nil synopsis_section assert_equal :section, synopsis_section.context assert synopsis_section.special assert_equal 'synopsis', synopsis_section.sectname end test 'should output special header block in HTML for manpage doctype' do input = <<-EOS = asciidoctor(1) :doctype: manpage == NAME asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats == SYNOPSIS *asciidoctor* ['OPTION']... 'FILE'.. EOS output = render_string input assert_css 'body.manpage', output, 1 assert_xpath '//body/*[@id="header"]/h1[text()="asciidoctor(1) Manual Page"]', output, 1 assert_xpath '//body/*[@id="header"]/h1/following-sibling::h2[text()="NAME"]', output, 1 assert_xpath '//h2[text()="NAME"]/following-sibling::*[@class="sectionbody"]', output, 1 assert_xpath '//h2[text()="NAME"]/following-sibling::*[@class="sectionbody"]/p[text()="asciidoctor - converts AsciiDoc source files to HTML, DocBook and other formats"]', output, 1 assert_xpath '//*[@id="content"]/*[@class="sect1"]/h2[text()="SYNOPSIS"]', output, 1 end end context 'Secure Asset Path' do test 'allows us to specify a path relative to the current dir' do doc = Asciidoctor::Document.new legit_path = Dir.pwd + '/foo' assert_equal legit_path, doc.normalize_asset_path(legit_path) end test 'keeps naughty absolute paths from getting outside' do naughty_path = "#{disk_root}etc/passwd" doc = Asciidoctor::Document.new secure_path = doc.normalize_asset_path(naughty_path) assert naughty_path != secure_path assert_match(/^#{doc.base_dir}/, secure_path) end test 'keeps naughty relative paths from getting outside' do naughty_path = 'safe/ok/../../../../../etc/passwd' doc = Asciidoctor::Document.new secure_path = doc.normalize_asset_path(naughty_path) assert naughty_path != secure_path assert_match(/^#{doc.base_dir}/, secure_path) end end end asciidoctor-0.1.4/test/extensions_test.rb000066400000000000000000000325121221220517700205630ustar00rootroot00000000000000require 'test_helper' require 'asciidoctor/extensions' class SamplePreprocessor < Asciidoctor::Extensions::Preprocessor end class SampleIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor end class SampleTreeprocessor < Asciidoctor::Extensions::Treeprocessor end class SamplePostprocessor < Asciidoctor::Extensions::Postprocessor end class SampleBlock < Asciidoctor::Extensions::BlockProcessor end class SampleBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor end class SampleInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor end class ScrubHeaderPreprocessor < Asciidoctor::Extensions::Preprocessor def process reader, lines while !lines.empty? && !lines.first.start_with?('=') lines.shift reader.advance end #lines reader end end class BoilerplateTextIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor def handles? target target.end_with? '.txt' end def process reader, target, attributes case target when 'lorem-ipsum.txt' content = ["Lorem ipsum dolor sit amet...\n"] reader.push_include content, target, target, 1, attributes else nil end end end class ReplaceAuthorTreeprocessor < Asciidoctor::Extensions::Treeprocessor def process @document.attributes['firstname'] = 'Ghost' @document.attributes['author'] = 'Ghost Writer' end end class StripAttributesPostprocessor < Asciidoctor::Extensions::Postprocessor def process output output.gsub(/<(\w+).*?>/m, "<\\1>") end end class UppercaseBlock < Asciidoctor::Extensions::BlockProcessor def process parent, reader, attributes Asciidoctor::Block.new parent, :paragraph, :source => reader.lines.map {|line| line.upcase } end end class SnippetMacro < Asciidoctor::Extensions::BlockMacroProcessor def process parent, target, attributes Asciidoctor::Block.new parent, :pass, :content_model => :raw, :source => %() end end class TemperatureMacro < Asciidoctor::Extensions::InlineMacroProcessor def process parent, target, attributes temperature_unit = @document.attr('temperature-unit', 'C') c = target.to_f if temperature_unit == 'C' text = %(#{c} °C) elsif temperature_unit == 'F' f = c * 1.8 + 32 text = %(#{f} °F) else text = target end text end end class SampleExtension < Asciidoctor::Extensions::Extension def self.activate(registry, document) document.attributes['activate-method-called'] = '' registry.preprocessor SamplePreprocessor end end context 'Extensions' do context 'Register' do test 'should register extension class' do begin Asciidoctor::Extensions.register SampleExtension assert_not_nil Asciidoctor::Extensions.registered assert_equal 1, Asciidoctor::Extensions.registered.size assert_equal SampleExtension, Asciidoctor::Extensions.registered.first ensure Asciidoctor::Extensions.unregister_all end end test 'should be able to self register extension class' do begin SampleExtension.register assert_not_nil Asciidoctor::Extensions.registered assert_equal 1, Asciidoctor::Extensions.registered.size assert_equal SampleExtension, Asciidoctor::Extensions.registered.first ensure Asciidoctor::Extensions.unregister_all end end test 'should register extension class from string' do begin Asciidoctor::Extensions.register 'SampleExtension' assert_not_nil Asciidoctor::Extensions.registered assert_equal 1, Asciidoctor::Extensions.registered.size assert_equal SampleExtension, Asciidoctor::Extensions.registered.first ensure Asciidoctor::Extensions.unregister_all end end test 'should register extension block' do begin Asciidoctor::Extensions.register do |document| end assert_not_nil Asciidoctor::Extensions.registered assert_equal 1, Asciidoctor::Extensions.registered.size assert Asciidoctor::Extensions.registered.first.is_a?(Proc) ensure Asciidoctor::Extensions.unregister_all end end test 'should get class for top-level class name' do clazz = Asciidoctor::Extensions.class_for_name('Asciidoctor') assert_not_nil clazz assert_equal Asciidoctor, clazz end test 'should get class for class name in module' do clazz = Asciidoctor::Extensions.class_for_name('Asciidoctor::Extensions') assert_not_nil clazz assert_equal Asciidoctor::Extensions, clazz end test 'should get class for class name resolved from root' do clazz = Asciidoctor::Extensions.class_for_name('::Asciidoctor::Extensions') assert_not_nil clazz assert_equal Asciidoctor::Extensions, clazz end test 'should raise exception if cannot find class for name' do begin Asciidoctor::Extensions.class_for_name('InvalidModule::InvalidClass') flunk 'Expecting RuntimeError to be raised' rescue RuntimeError => e assert_equal 'Could not resolve class for name: InvalidModule::InvalidClass', e.message end end test 'should resolve class if class is given' do clazz = Asciidoctor::Extensions.resolve_class(Asciidoctor::Extensions) assert_not_nil clazz assert_equal Asciidoctor::Extensions, clazz end test 'should resolve class if class from string' do clazz = Asciidoctor::Extensions.resolve_class('Asciidoctor::Extensions') assert_not_nil clazz assert_equal Asciidoctor::Extensions, clazz end end context 'Activate' do test 'should call activate on extension class' do begin doc = Asciidoctor::Document.new Asciidoctor::Extensions.register SampleExtension registry = Asciidoctor::Extensions::Registry.new doc assert doc.attr? 'activate-method-called' assert registry.preprocessors? ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke extension block' do begin doc = Asciidoctor::Document.new Asciidoctor::Extensions.register do |document| document.attributes['block-called'] = '' preprocessor SamplePreprocessor end registry = Asciidoctor::Extensions::Registry.new doc assert doc.attr? 'block-called' assert registry.preprocessors? ensure Asciidoctor::Extensions.unregister_all end end test 'should create registry in Document if extensions are loaded' do begin SampleExtension.register doc = Asciidoctor::Document.new assert doc.extensions? assert doc.extensions.is_a? Asciidoctor::Extensions::Registry ensure Asciidoctor::Extensions.unregister_all end end end context 'Instantiate' do test 'should instantiate preprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.preprocessor SamplePreprocessor assert registry.preprocessors? processors = registry.load_preprocessors Asciidoctor::Document.new assert_equal 1, processors.size assert processors.first.is_a? SamplePreprocessor end test 'should instantiate include processors' do registry = Asciidoctor::Extensions::Registry.new registry.include_processor SampleIncludeProcessor assert registry.include_processors? processors = registry.load_include_processors Asciidoctor::Document.new assert_equal 1, processors.size assert processors.first.is_a? SampleIncludeProcessor end test 'should instantiate treeprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.treeprocessor SampleTreeprocessor assert registry.treeprocessors? processors = registry.load_treeprocessors Asciidoctor::Document.new assert_equal 1, processors.size assert processors.first.is_a? SampleTreeprocessor end test 'should instantiate postprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.postprocessor SamplePostprocessor assert registry.postprocessors? processors = registry.load_postprocessors Asciidoctor::Document.new assert_equal 1, processors.size assert processors.first.is_a? SamplePostprocessor end test 'should instantiate block processor' do registry = Asciidoctor::Extensions::Registry.new registry.block :sample, SampleBlock assert registry.blocks? assert registry.processor_registered_for_block? :sample, :paragraph processor = registry.load_block_processor :sample, Asciidoctor::Document.new assert processor.is_a? SampleBlock end test 'should not match block processor for unsupported context' do registry = Asciidoctor::Extensions::Registry.new registry.block :sample, SampleBlock assert !(registry.processor_registered_for_block? :sample, :sidebar) end test 'should instantiate block macro processor' do registry = Asciidoctor::Extensions::Registry.new registry.block_macro 'sample', SampleBlockMacro assert registry.block_macros? assert registry.processor_registered_for_block_macro? 'sample' processor = registry.load_block_macro_processor 'sample', Asciidoctor::Document.new assert processor.is_a? SampleBlockMacro end test 'should instantiate inline macro processor' do registry = Asciidoctor::Extensions::Registry.new registry.inline_macro 'sample', SampleInlineMacro assert registry.inline_macros? assert registry.processor_registered_for_inline_macro? 'sample' processor = registry.load_inline_macro_processor 'sample', Asciidoctor::Document.new assert processor.is_a? SampleInlineMacro end test 'should allow processors to be registered by a string name' do registry = Asciidoctor::Extensions::Registry.new registry.preprocessor 'SamplePreprocessor' assert registry.preprocessors? processors = registry.load_preprocessors Asciidoctor::Document.new assert_equal 1, processors.size assert processors.first.is_a? SamplePreprocessor end end context 'Integration' do test 'should invoke preprocessors before parsing document' do input = <<-EOS junk line = Document Title sample content EOS begin Asciidoctor::Extensions.register do |document| preprocessor ScrubHeaderPreprocessor end doc = document_from_string input assert doc.has_header? assert_equal 'Document Title', doc.doctitle ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke include processor to process include macro' do input = <<-EOS before include::lorem-ipsum.txt[] after EOS begin Asciidoctor::Extensions.register do |document| include_processor BoilerplateTextIncludeProcessor end result = render_string input, :safe => :server assert_css '.paragraph > p', result, 3 assert result.include?('before') assert result.include?('Lorem ipsum') assert result.include?('after') ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke treeprocessors after parsing document' do input = <<-EOS = Document Title Doc Writer content EOS begin Asciidoctor::Extensions.register do |document| treeprocessor ReplaceAuthorTreeprocessor end doc = document_from_string input assert_equal 'Ghost Writer', doc.author ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke postprocessors after rendering document' do input = <<-EOS * one * two * three EOS begin Asciidoctor::Extensions.register do |document| postprocessor StripAttributesPostprocessor end output = render_string input assert_no_match(/
    /, output) ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke processor for custom block' do input = <<-EOS [yell] Hi there! EOS begin Asciidoctor::Extensions.register do |document| block :yell, UppercaseBlock end output = render_embedded_string input assert_xpath '//p', output, 1 assert_xpath '//p[text()="HI THERE!"]', output, 1 ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke processor for custom block macro' do input = <<-EOS snippet::12345[] EOS begin Asciidoctor::Extensions.register do |document| block_macro :snippet, SnippetMacro end output = render_embedded_string input assert output.include?('') ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke processor for custom inline macro' do input = <<-EOS Room temperature is degrees:25[]. EOS begin Asciidoctor::Extensions.register do |document| inline_macro :degrees, TemperatureMacro end output = render_embedded_string input, :attributes => {'temperature-unit' => 'F'} assert output.include?('Room temperature is 77.0 °F.') ensure Asciidoctor::Extensions.unregister_all end end end end asciidoctor-0.1.4/test/fixtures/000077500000000000000000000000001221220517700166465ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/asciidoc.txt000066400000000000000000000067251221220517700211770ustar00rootroot00000000000000AsciiDoc User Guide =================== Stuart Rackham :Author Initials: SJR :toc: :icons: :numbered: :website: http://www.methods.co.nz/asciidoc/ AsciiDoc is a text document format for writing notes, documentation, articles, books, ebooks, slideshows, web pages, blogs and UNIX man pages. .This document ********************************************************************** This is an overly large document, it probably needs to be refactored into a Tutorial, Quick Reference and Formal Reference. If you're new to AsciiDoc read this section and the <> section and take a look at the example AsciiDoc (`*.txt`) source files in the distribution `doc` directory. ********************************************************************** Introduction ------------ AsciiDoc is a plain text human readable/writable document format that can be translated to DocBook or HTML using the asciidoc(1) command. asciidoc(1) comes with a set of configuration files to translate AsciiDoc articles, books and man pages to HTML or DocBook backend formats. .My AsciiDoc Itch ********************************************************************** DocBook has emerged as the de facto standard Open Source documentation format. But DocBook is a complex language, the markup is difficult to read and even more difficult to write directly -- I found I was and fixing syntax errors, than I was writing the documentation. ********************************************************************** [[X6]] Getting Started --------------- Installing AsciiDoc ~~~~~~~~~~~~~~~~~~~ See the `README` and `INSTALL` files for install prerequisites and procedures. Packagers take a look at <>. [[X11]] Example AsciiDoc Documents ~~~~~~~~~~~~~~~~~~~~~~~~~~ The best way to quickly get a feel for AsciiDoc is to view the AsciiDoc web site and/or distributed examples: - Take a look at the linked examples on the AsciiDoc web site home page {website}. Press the 'Page Source' sidebar menu item to view corresponding AsciiDoc source. - Read the `*.txt` source files in the distribution `./doc` directory along with the corresponding HTML and DocBook XML files. AsciiDoc Document Types ----------------------- There are three types of AsciiDoc documents: article, book and manpage. Use the asciidoc(1) `-d` (`--doctype`) option to specify the AsciiDoc document type -- the default document type is 'article'. article ~~~~~~~ Used for short documents, articles and general documentation. See the AsciiDoc distribution `./doc/article.txt` example. AsciiDoc defines standard DocBook article frontmatter and backmatter <> (appendix, abstract, bibliography, glossary, index). book ~~~~ Books share the same format as articles, with the following differences: - The part titles in multi-part books are <> (same level as book title). - Some sections are book specific e.g. preface and colophon. Book documents will normally be used to produce DocBook output since DocBook processors can automatically generate footnotes, table of contents, list of tables, list of figures, list of examples and indexes. AsciiDoc defines standard DocBook book frontmatter and backmatter <> (appendix, dedication, preface, bibliography, glossary, index, colophon). .Example book documents Book:: The `./doc/book.txt` file in the AsciiDoc distribution. Multi-part book:: The `./doc/book-multi.txt` file in the AsciiDoc distribution. asciidoctor-0.1.4/test/fixtures/asciidoc_index.txt000066400000000000000000000520551221220517700223630ustar00rootroot00000000000000AsciiDoc Home Page ================== // Web page meta data. :keywords: AsciiDoc, DocBook, EPUB, PDF, ebooks, slideshow, slidy, man page :description: AsciiDoc is a text document format for writing notes, + documentation, articles, books, ebooks, slideshows, + web pages, man pages and blogs. AsciiDoc files can be + translated to many formats including HTML, PDF, EPUB, + man page. .{revdate}: AsciiDoc {revnumber} Released ************************************************************************ Read the link:CHANGELOG.html[CHANGELOG] for release highlights and a full list of all additions, changes and bug fixes. Changes are documented in the updated link:userguide.html[User Guide]. See the link:INSTALL.html[Installation page] for downloads and and installation instructions. 'Stuart Rackham' ************************************************************************ Introduction ------------ {description} AsciiDoc is highly configurable: both the AsciiDoc source file syntax and the backend output markups (which can be almost any type of SGML/XML markup) can be customized and extended by the user. AsciiDoc is free software and is licenced under the terms of the 'GNU General Public License version 2' (GPLv2). TIP: The pages you are reading were written using AsciiDoc, to view the corresponding AsciiDoc source click on the *Page Source* menu item in the left hand margin. Overview and Examples --------------------- You write an AsciiDoc document the same way you would write a normal text document, there are no markup tags or weird format notations. AsciiDoc files are designed to be viewed, edited and printed directly or translated to other presentation formats using the asciidoc(1) command. The asciidoc(1) command translates AsciiDoc files to HTML, XHTML and DocBook markups. DocBook can be post-processed to presentation formats such as HTML, PDF, EPUB, DVI, LaTeX, roff, and Postscript using readily available Open Source tools. Example Articles ~~~~~~~~~~~~~~~~ - This XHTML version of the link:asciidoc.css-embedded.html[AsciiDoc User Guide] was generated by AsciiDoc from link:asciidoc.txt[this AsciiDoc file]. - Here's the link:asciidoc.html[same document] created by first generating DocBook markup using AsciiDoc and then converting the DocBook markup to HTML using 'DocBook XSL Stylesheets'. - The User Guide again, this time a link:chunked/index.html[chunked version]. - AsciiDoc generated this link:article-standalone.html[stand-alone HTML file] containing embedded CSS, JavaScript and images from this link:article.txt[AsciiDoc article template] with this command: asciidoc -a data-uri -a icons -a toc -a max-width=55em article.txt - The same link:article.txt[AsciiDoc article template] generated link:article-html5-toc2.html[this HTML 5] (the 'toc2' attribute puts a table of contents in the left margin) from this command: asciidoc -b html5 -a icons -a toc2 -a theme=flask article.txt - The same link:article.txt[AsciiDoc article template] produced this link:article.html[HTML file] and this link:article.pdf[PDF file] via DocBook markup generated by AsciiDoc. [[X7]] Example Books ~~~~~~~~~~~~~ AsciiDoc markup supports all the standard DocBook frontmatter and backmatter sections (dedication, preface, bibliography, glossary, index, colophon) plus footnotes and index entries. - This link:book.txt[AsciiDoc book] produced link:book.html[this HTML file] using the 'DocBook XSL Stylesheets'. - The link:asciidoc.pdf[PDF formatted AsciiDoc User Guide] was generated from asciidoc(1) DocBook output. - The link:asciidoc.epub[EPUB formatted AsciiDoc User Guide] was generated using link:a2x.1.html[a2x]. - This link:book.epub[EPUB formatted book skeleton] was generated using link:a2x.1.html[a2x]. - This link:book-multi.txt[multi-part AsciiDoc book] produced link:book-multi.html[this HTML file] using the 'DocBook XSL Stylesheets'. Example UNIX Man Pages ~~~~~~~~~~~~~~~~~~~~~~ HTML formatted AsciiDoc man pages link:asciidoc.1.css-embedded.html[with stylesheets] and link:asciidoc.1.html[without stylesheets] were generated by AsciiDoc from link:asciidoc.1.txt[this file]. This link:asciidoc.1[roff formatted man page] was generated from asciidoc(1) DocBook output using `xsltproc(1)` and DocBook XSL Stylesheets. [[X8]] Example Slideshows ~~~~~~~~~~~~~~~~~~ The http://www.w3.org/Talks/Tools/Slidy2/[Slidy] backend generates HTML slideshows that can be viewed in any web browser. What's nice is that you can create completely self contained slideshows including embedded images. - Here is the link:slidy.html[slidy backend documentation] slideshow and here is it's link:slidy.txt[AsciiDoc source]. - An link:slidy-example.html[example slidy slideshow] and the link:slidy-example.txt[AsciiDoc source]. Example Web Site ~~~~~~~~~~~~~~~~ The link:README-website.html[AsciiDoc website] is included in the AsciiDoc distribution (in `./examples/website/`) as an example website built using AsciiDoc. See `./examples/website/README-website.txt`. More examples ~~~~~~~~~~~~~ - See below: <>. - Example link:newtables.html[Tables]. eBook Publication ----------------- The two most popular open eBook formats are http://en.wikipedia.org/wiki/EPUB[EPUB] and PDF. The AsciiDoc link:a2x.1.html[a2x] toolchain wrapper makes it easy to link:publishing-ebooks-with-asciidoc.html[publish EPUB and PDF eBooks with AsciiDoc]. See also <> and link:epub-notes.html[AsciiDoc EPUB Notes]). Blogpost weblog client ---------------------- http://srackham.wordpress.com/blogpost-readme/[blogpost] is a command-line weblog client for publishing AsciiDoc documents to http://wordpress.org/[WordPress] blog hosts. It creates and updates weblog posts and pages directly from AsciiDoc source documents. Source code highlighter ----------------------- AsciiDoc includes a link:source-highlight-filter.html[source code highlighter filter] that uses http://www.gnu.org/software/src-highlite/[GNU source-highlight] to highlight HTML outputs. You also have the option of using the http://pygments.org/[Pygments] highlighter. [[X3]] Mathematical Formulae --------------------- You can include mathematical formulae in AsciiDoc XHTML documents using link:asciimathml.html[ASCIIMathML] or link:latexmathml.html[LaTeXMathML] notation. The link:latex-filter.html[AsciiDoc LaTeX filter] translates LaTeX source to a PNG image that is automatically inserted into the AsciiDoc output documents. AsciiDoc also has 'latexmath' macros for DocBook outputs -- they are documented in link:latexmath.pdf[this PDF file] and can be used in AsciiDoc documents processed by `dblatex(1)`. Editor Support -------------- - An AsciiDoc syntax highlighter for the Vim text editor is included in the AsciiDoc distribution (see 'Appendix F' of the 'AsciiDoc User Guide' for details). + .Syntax highlighter screenshot image::images/highlighter.png[height=400,caption="",link="images/highlighter.png"] - Dag Wieers has implemented an alternative Vim syntax file for AsciiDoc which can be found here http://svn.rpmforge.net/svn/trunk/tools/asciidoc-vim/. - David Avsajanishvili has written a source highlighter for AsciiDoc files for http://projects.gnome.org/gtksourceview/[GtkSourceView] (used by http://projects.gnome.org/gedit/[gedit] and a number of other applications). The project is hosted here: https://launchpad.net/asciidoc-gtk-highlight - Florian Kaufman has written 'adoc-mode.el' -- a major-mode for editing AsciiDoc files in Emacs, you can find it http://code.google.com/p/sensorflo-emacs/[here]. - The http://xpt.sourceforge.net/[*Nix Power Tools project] has released an http://xpt.sourceforge.net/tools/doc-mode/[AsciiDoc syntax highlighter for Emacs]. - Terrence Brannon has written http://github.com/metaperl/asciidoc-el[AsciiDoc functions for Emacs]. - Christian Zuckschwerdt has written a https://github.com/zuckschwerdt/asciidoc.tmbundle[TextMate bundle] for AsciiDoc. Try AsciiDoc on the Web ----------------------- Andrew Koster has written a Web based application to interactively convert and display AsciiDoc source: http://andrewk.webfactional.com/asciidoc.php [[X2]] External Resources and Applications ----------------------------------- Here are resources that I know of, if you know of more drop me a line and I'll add them to the list. - Check the link:INSTALL.html#X2[installation page] for packaged versions of AsciiDoc. - Alex Efros has written an HTML formatted http://powerman.name/doc/asciidoc[AsciiDoc Cheatsheet] using Asciidoc. - Thomas Berker has written an http://liksom.info/blog/?q=node/114[AsciiDoc Cheatsheet] in Open Document and PDF formats. - The http://www.wikimatrix.org/[WikiMatrix] website has an excellent http://www.wikimatrix.org/syntax.php[web page] that compares the various Wiki markup syntaxes. An interesting attempt at Wiki markup standardization is http://www.wikicreole.org/[CREOLE]. - Franck Pommereau has written http://www.univ-paris12.fr/lacl/pommereau/soft/asciidoctest.html[Asciidoctest], a program that doctests snippets of Python code within your Asciidoc documents. - The http://remips.sourceforge.net/[ReMIPS] project website has been built using AsciiDoc. - Here are some link:asciidoc-docbook-xsl.html[DocBook XSL Stylesheets Notes]. - Karl Mowatt-Wilson has developed an http://ikiwiki.info/[ikiwiki] plugin for AsciiDoc which he uses to render http://mowson.org/karl[his website]. The plugin is available http://www.mowson.org/karl/colophon/[here] and there is some discussion of the ikiwiki integration http://ikiwiki.info/users/KarlMW/discussion/[here]. - Glenn Eychaner has http://groups.google.com/group/asciidoc/browse_thread/thread/bf04b55628efe214[reworked the Asciidoc plugin for ikiwiki] that was created by Karl Mowson, the source can be downloaded from http://dl.dropbox.com/u/11256359/asciidoc.pm - David Hajage has written an AsciiDoc package for the http://www.r-project.org/[R Project] (R is a free software environment for statistical computing). 'ascii' is available on 'CRAN' (just run `install.package("ascii")` from R). Briefly, 'ascii' replaces R results in AsciiDoc document with AsciiDoc markup. More information and examples here: http://eusebe.github.com/ascii/. - Pascal Rapaz has written a Python script to automate AsciiDoc website generation. You can find it at http://www.rapazp.ch/opensource/tools/asciidoc.html. - Jared Henley has written http://jared.henley.id.au/software/awb/documentation.html[AsciiDoc Website Builder]. 'AsciiDoc Website Builder' (awb) is a python program that automates the building of of a website written in AsciiDoc. All you need to write is the AsciiDoc source plus a few simple configuration files. - Brad Adkins has written http://dbixjcl.org/jcl/asciidocgen/asciidocgen.html[AsciiDocGen], a web site generation and deployment tool that allows you write your web site content in AsciiDoc. The http://dbixjcl.org/jcl/asciidocgen/asciidocgen.html[AsciiDocGen web site] is managed using 'AsciiDocGen'. - Filippo Negroni has developed a set of tools to facilitate 'literate programming' using AsciiDoc. The set of tools is called http://eweb.sourceforge.net/[eWEB]. - http://vanderwijk.info/2009/4/23/full-text-based-document-generation-using-asciidoc-and-ditaa[Ivo's blog] describes a http://ditaa.sourceforge.net/[ditaa] filter for AsciiDoc which converts http://en.wikipedia.org/wiki/ASCII_art[ASCII art] into graphics. - http://github.com/github/gollum[Gollum] is a git-powered wiki, it supports various formats, including AsciiDoc. - Gregory Romé has written an http://github.com/gpr/redmine_asciidoc_formatter[AsciiDoc plugin] for the http://www.redmine.org/[Redmine] project management application. - Paul Hsu has started a http://github.com/paulhsu/AsciiDoc.CHT.userguide[Chinese translation of the AsciiDoc User Guide]. - Dag Wieers has written http://dag.wieers.com/home-made/unoconv/[UNOCONV]. 'UNOCONV' can export AsciiDoc outputs to OpenOffice export formats. - Ed Keith has written http://codeextactor.berlios.de/[Code Extractor], it extracts code snippets from source code files and inserts them into AsciiDoc documents. - The http://csrp.iut-blagnac.fr/jmiwebsite/home/[JMI website] hosts a number of extras for AsciiDoc and Slidy written by Jean-Michel Inglebert. - Ryan Tomayko has written an number of http://tomayko.com/src/adoc-themes/[themes for AsciiDoc] along with a http://tomayko.com/src/adoc-themes/hacking.html[script for combining the CSS files] into single CSS theme files for AsciiDoc embedded CSS documents. - Ilya Portnov has written a https://gitorious.org/doc-building-system[document building system for AsciiDoc], here is http://iportnov.blogspot.com/2011/03/asciidoc-beamer.html[short article in Russian] describing it. - Lex Trotman has written https://github.com/elextr/codiicsa[codiicsa], a program that converts DocBook to AsciiDoc. - Qingping Hou has written http://houqp.github.com/asciidoc-deckjs/[an AsciiDoc backend for deck.js]. http://imakewebthings.github.com/deck.js/[deck.js] is a JavaScript library for building modern HTML presentations (slideshows). - The guys from O'Reilly Media have posted an https://github.com/oreillymedia/docbook2asciidoc[XSL Stylesheet to github] that converts DocBook to AsciiDoc. Please let me know if any of these links need updating. [[X6]] Documents written using AsciiDoc -------------------------------- Here are some documents I know of, if you know of more drop me a line and I'll add them to the list. - The book http://practicalunittesting.com/[Practical Unit Testing] by Tomek Kaczanowski was https://groups.google.com/group/asciidoc/browse_frm/thread/4ba13926262efa23[written using Asciidoc]. - The book http://oreilly.com/catalog/9781449397296[Programming iOS 4] by Matt Neuburg was written using AsciiDoc. Matt has http://www.apeth.net/matt/iosbooktoolchain.html[written an article] describing how he used AsciiDoc and other tools to write the book. - The book http://oreilly.com/catalog/9780596155957/index.html[Programming Scala] by Dean Wampler and Alex Payne (O'Reilly) was http://groups.google.com/group/asciidoc/browse_frm/thread/449f1199343f0e27[written using Asciidoc]. - The http://www.ncfaculty.net/dogle/fishR/index.html[fishR] website has a number of http://www.ncfaculty.net/dogle/fishR/bookex/AIFFD/AIFFD.html[book examples] written using AsciiDoc. - The Neo4j graph database project uses Asciidoc, and the output is published here: http://docs.neo4j.org/. The build process includes live tested source code snippets and is described http://groups.google.com/group/asciidoc/browse_thread/thread/49d570062fd3ff52[here]. - http://frugalware.org/[Frugalware Linux] uses AsciiDoc for http://frugalware.org/docs[documentation]. - http://www.cherokee-project.com/doc/[Cherokee documentation]. - Henrik Maier produced this professional User manual using AsciiDoc: http://www.proconx.com/assets/files/products/modg100/UMMBRG300-1101.pdf - Henrik also produced this folded single page brochure format example: http://www.proconx.com/assets/files/products/modg100/IGMBRG300-1101-up.pdf + See this http://groups.google.com/group/asciidoc/browse_thread/thread/16ab5a06864b934f[AsciiDoc discussion group thread] for details. - The http://www.kernel.org/pub/software/scm/git/docs/user-manual.html[Git User's Manual]. - 'Git Magic' + http://www-cs-students.stanford.edu/~blynn/gitmagic/ + http://github.com/blynn/gitmagic/tree/1e5780f658962f8f9b01638059b27275cfda095c - 'CouchDB: The Definitive Guide' + http://books.couchdb.org/relax/ + http://groups.google.com/group/asciidoc/browse_thread/thread/a60f67cbbaf862aa/d214bf7fa2d538c4?lnk=gst&q=book#d214bf7fa2d538c4 - 'Ramaze Manual' + http://book.ramaze.net/ + http://github.com/manveru/ramaze-book/tree/master - Some documentation about git by Nico Schottelius (in German) http://nico.schotteli.us/papers/linux/git-firmen/. - The http://www.netpromi.com/kirbybase_ruby.html[KirbyBase for Ruby] database management system manual. - The http://xpt.sourceforge.net/[*Nix Power Tools project] uses AsciiDoc for documentation. - The http://www.wesnoth.org/[Battle for Wesnoth] project uses AsciiDoc for its http://www.wesnoth.org/wiki/WesnothManual[Manual] in a number of different languages. - Troy Hanson uses AsciiDoc to generate user guides for the http://tpl.sourceforge.net/[tpl] and http://uthash.sourceforge.net/[uthash] projects (the HTML versions have a customised contents sidebar). - http://volnitsky.com/[Leonid Volnitsky's site] is generated using AsciiDoc and includes Leonid's matplotlib filter. - http://www.weechat.org/[WeeChat] uses AsciiDoc for http://www.weechat.org/doc[project documentation]. - http://www.clansuite.com/[Clansuite] uses AsciiDoc for http://www.clansuite.com/documentation/[project documentation]. - The http://fc-solve.berlios.de/[Freecell Solver program] uses AsciiDoc for its http://fc-solve.berlios.de/docs/#distributed-docs[distributed documentation]. - Eric Raymond's http://gpsd.berlios.de/AIVDM.html[AIVDM/AIVDO protocol decoding] documentation is written using AsciiDoc. - Dwight Schauer has written an http://lxc.teegra.net/[LXC HOWTO] in AsciiDoc. - The http://www.rowetel.com/ucasterisk/[Free Telephony Project] website is generated using AsciiDoc. - Warren Block has http://www.wonkity.com/~wblock/docs/[posted a number of articles written using AsciiDoc]. - The http://code.google.com/p/waf/[Waf project's] 'Waf Book' is written using AsciiDoc, there is an http://waf.googlecode.com/svn/docs/wafbook/single.html[HTML] and a http://waf.googlecode.com/svn/docs/wafbook/waf.pdf[PDF] version. - The http://www.diffkit.org/[DiffKit] project's documentation and website have been written using Asciidoc. - The http://www.networkupstools.org[Network UPS Tools] project http://www.networkupstools.org/documentation.html[documentation] is an example of a large documentation project written using AsciiDoc. - http://www.archlinux.org/pacman/[Pacman], the http://www.archlinux.org/[Arch Linux] package manager, has been documented using AsciiDoc. - Suraj Kurapati has written a number of customized manuals for his Open Source projects using AsciiDoc: * http://snk.tuxfamily.org/lib/detest/ * http://snk.tuxfamily.org/lib/ember/ * http://snk.tuxfamily.org/lib/inochi/ * http://snk.tuxfamily.org/lib/rumai/ - The http://cxxtest.com/[CxxTest] project (unit testing for C++ language) has written its User Guide using AsciiDoc. Please let me know if any of these links need updating. DocBook 5.0 Backend ------------------- Shlomi Fish has begun work on a DocBook 5.0 `docbook50.conf` backend configuration file, you can find it http://bitbucket.org/shlomif/asciidoc[here]. See also: http://groups.google.com/group/asciidoc/browse_thread/thread/4386c7cc053d51a9 [[X1]] LaTeX Backend ------------- An experimental LaTeX backend was written for AsciiDoc in 2006 by Benjamin Klum. Benjamin did a superhuman job (I admit it, I didn't think this was doable due to AsciiDoc's SGML/XML bias). Owning to to other commitments, Benjamin was unable to maintain this backend. Here's link:latex-backend.html[Benjamin's original documentation]. Incompatibilities introduced after AsciiDoc 8.2.7 broke the LaTeX backend. In 2009 Geoff Eddy stepped up and updated the LaTeX backend, thanks to Geoff's efforts it now works with AsciiDoc 8.4.3. Geoff's updated `latex.conf` file shipped with AsciiDoc version 8.4.4. The backend still has limitations and remains experimental (see link:latex-bugs.html[Geoff's notes]). It's probably also worth pointing out that LaTeX output can be generated by passing AsciiDoc generated DocBook through `dblatex(1)`. Patches and bug reports ----------------------- Patches and bug reports are are encouraged, but please try to follow these guidelines: - Post bug reports and patches to the http://groups.google.com/group/asciidoc[asciidoc discussion list], this keeps things transparent and gives everyone a chance to comment. - The email subject line should be a specific and concise topic summary. Commonly accepted subject line prefixes such as '[ANN]', '[PATCH]' and '[SOLVED]' are good. === Bug reports - When reporting problems please illustrate the problem with the smallest possible example that replicates the issue (and please test your example before posting). This technique will also help to eliminate red herrings prior to posting. - Paste the commands that you executed along with any relevant outputs. - Include the version of AsciiDoc and the platform you're running it on. - If you can program please consider writing a patch to fix the problem. === Patches - Keep patches small and atomic (one issue per patch) -- no patch bombs. - If possible test your patch against the current trunk. - If your patch adds or modifies functionality include a short example that illustrates the changes. - Send patches in `diff -u` format, inline inside the mail message is usually best; if it is a very long patch then send it as an attachment. - Include documentation updates if you're up to it; otherwise insert 'TODO' comments at relevant places in the documentation. asciidoctor-0.1.4/test/fixtures/ascshort.txt000066400000000000000000000020751221220517700212410ustar00rootroot00000000000000AsciiDoc User Guide =================== Stuart Rackham :Author Initials: SJR :toc: :icons: :numbered: :website: http://www.methods.co.nz/asciidoc/ AsciiDoc is a text document format for writing notes, documentation, articles, books, ebooks, slideshows, web pages, blogs and UNIX man pages. .This document ********************************************************************** This is an overly large document, it probably needs to be refactored into a Tutorial, Quick Reference and Formal Reference. If you're new to AsciiDoc read this section and the <> section and take a look at the example AsciiDoc (`*.txt`) source files in the distribution `doc` directory. ********************************************************************** Introduction ------------ AsciiDoc is a plain text human readable/writable document format that can be translated to DocBook or HTML using the asciidoc(1) command. asciidoc(1) comes with a set of configuration files to translate AsciiDoc articles, books and man pages to HTML or DocBook backend formats. asciidoctor-0.1.4/test/fixtures/basic-docinfo-footer.html000066400000000000000000000003571221220517700235350ustar00rootroot00000000000000 asciidoctor-0.1.4/test/fixtures/basic-docinfo-footer.xml000066400000000000000000000003021221220517700233570ustar00rootroot00000000000000 {revnumber} 01 Jan 2013 abc Unleashed into the wild asciidoctor-0.1.4/test/fixtures/basic-docinfo.html000066400000000000000000000000451221220517700222330ustar00rootroot00000000000000 asciidoctor-0.1.4/test/fixtures/basic-docinfo.xml000066400000000000000000000001601221220517700220650ustar00rootroot00000000000000 2013 Acme, Inc. asciidoctor-0.1.4/test/fixtures/basic.asciidoc000066400000000000000000000001261221220517700214260ustar00rootroot00000000000000= Document Title Doc Writer v1.0, 2013-01-01 Body content. asciidoctor-0.1.4/test/fixtures/child-include.adoc000066400000000000000000000001141221220517700221760ustar00rootroot00000000000000first line of child include::grandchild-include.adoc[] last line of child asciidoctor-0.1.4/test/fixtures/custom-backends/000077500000000000000000000000001221220517700217305ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/haml/000077500000000000000000000000001221220517700226515ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/haml/docbook45/000077500000000000000000000000001221220517700244425ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/haml/docbook45/block_paragraph.xml.haml000066400000000000000000000003141221220517700312210ustar00rootroot00000000000000- if title? %formalpara{:id=>@id, :role=>(attr :role), :xreflabel=>(attr :reftext)} %title=title %para=content - else %para{:id=>@id, :role=>(attr :role), :xreflabel=>(attr :reftext)}=content asciidoctor-0.1.4/test/fixtures/custom-backends/haml/html5-tweaks/000077500000000000000000000000001221220517700251765ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/haml/html5-tweaks/block_paragraph.html.haml000066400000000000000000000000131221220517700321150ustar00rootroot00000000000000%p=content asciidoctor-0.1.4/test/fixtures/custom-backends/haml/html5/000077500000000000000000000000001221220517700237025ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/haml/html5/block_paragraph.html.haml000066400000000000000000000001071221220517700306250ustar00rootroot00000000000000- if title? .title=title %p{:id=>@id, :class=>(attr 'role')}=content asciidoctor-0.1.4/test/fixtures/custom-backends/haml/html5/block_sidebar.html.haml000066400000000000000000000001431221220517700302710ustar00rootroot00000000000000%aside{:id=>@id, :class=>(attr 'role')} - if title? %header %h1=title =content.chomp asciidoctor-0.1.4/test/fixtures/custom-backends/slim/000077500000000000000000000000001221220517700226745ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/slim/docbook45/000077500000000000000000000000001221220517700244655ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/slim/docbook45/block_paragraph.xml.slim000066400000000000000000000002671221220517700312760ustar00rootroot00000000000000- if title? formalpara id=@id role=(attr :role) xreflabel=(attr :reftext) title=title para=content - else para id=@id role=(attr :role) xreflabel=(attr :reftext) =content asciidoctor-0.1.4/test/fixtures/custom-backends/slim/html5/000077500000000000000000000000001221220517700237255ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/custom-backends/slim/html5/block_paragraph.html.slim000066400000000000000000000001011221220517700306650ustar00rootroot00000000000000- if title? .title=title p id=@id class=(attr 'role') =content asciidoctor-0.1.4/test/fixtures/custom-backends/slim/html5/block_sidebar.html.slim000066400000000000000000000001241221220517700303360ustar00rootroot00000000000000aside id=@id class=(attr 'role') - if title? header h1=title =content asciidoctor-0.1.4/test/fixtures/docinfo-footer.html000066400000000000000000000000451221220517700224500ustar00rootroot00000000000000Back to top asciidoctor-0.1.4/test/fixtures/docinfo-footer.xml000066400000000000000000000002521221220517700223040ustar00rootroot00000000000000 Glossary term definition asciidoctor-0.1.4/test/fixtures/docinfo.html000066400000000000000000000000611221220517700211520ustar00rootroot00000000000000 asciidoctor-0.1.4/test/fixtures/docinfo.xml000066400000000000000000000001531221220517700210100ustar00rootroot00000000000000Asciidoctor 1.0.0 {revnumber} asciidoctor-0.1.4/test/fixtures/dot.gif000066400000000000000000000000431221220517700201200ustar00rootroot00000000000000GIF89a€,D;asciidoctor-0.1.4/test/fixtures/encoding.asciidoc000066400000000000000000000006501221220517700221350ustar00rootroot00000000000000Gregory Romé has written an AsciiDoc plugin for the Redmine project management application. https://github.com/foo-users/foo ã¸ã¨ `vicmd` キーマップを足ã—ã¦ã¿ã¦ã„る試ã¿ã€ アニメーションgifã§ã™ã€‚ tag::romé[] Gregory Romé has written an AsciiDoc plugin for the Redmine project management application. end::romé[] == Überschrift * Codierungen sind verrückt auf älteren Versionen von Ruby asciidoctor-0.1.4/test/fixtures/grandchild-include.adoc000066400000000000000000000000621221220517700232140ustar00rootroot00000000000000first line of grandchild last line of grandchild asciidoctor-0.1.4/test/fixtures/include-file.asciidoc000066400000000000000000000006711221220517700227120ustar00rootroot00000000000000first line of included content second line of included content third line of included content fourth line of included content fifth line of included content sixth line of included content seventh line of included content eighth line of included content // tag::snippetA[] snippetA content // end::snippetA[] non-tagged content // tag::snippetB[] snippetB content // end::snippetB[] more non-tagged content last line of included content asciidoctor-0.1.4/test/fixtures/list_elements.asciidoc000066400000000000000000000001461221220517700232160ustar00rootroot00000000000000AsciiDoc Home Page ================== Example Articles ~~~~~~~~~~~~~~~~ - Item 1 - Item 2 - Item 3 asciidoctor-0.1.4/test/fixtures/parent-include-restricted.adoc000066400000000000000000000001201221220517700245470ustar00rootroot00000000000000first line of parent include::child-include.adoc[depth=1] last line of parent asciidoctor-0.1.4/test/fixtures/parent-include.adoc000066400000000000000000000001111221220517700224010ustar00rootroot00000000000000first line of parent include::child-include.adoc[] last line of parent asciidoctor-0.1.4/test/fixtures/sample.asciidoc000066400000000000000000000005001221220517700216220ustar00rootroot00000000000000Document Title ============== Doc Writer :idprefix: id_ Preamble paragraph. NOTE: This is test, only a test. == Section A *Section A* paragraph. === Section A Subsection *Section A* 'subsection' paragraph. == Section B *Section B* paragraph. .Section B list * Item 1 * Item 2 * Item 3 asciidoctor-0.1.4/test/fixtures/stylesheets/000077500000000000000000000000001221220517700212225ustar00rootroot00000000000000asciidoctor-0.1.4/test/fixtures/stylesheets/custom.css000066400000000000000000000000271221220517700232450ustar00rootroot00000000000000body { color: red; } asciidoctor-0.1.4/test/fixtures/tip.gif000066400000000000000000000000431221220517700201260ustar00rootroot00000000000000GIF89a€,D;asciidoctor-0.1.4/test/invoker_test.rb000066400000000000000000000376251221220517700200530ustar00rootroot00000000000000# encoding: UTF-8 require 'test_helper' require 'asciidoctor/cli/options' require 'asciidoctor/cli/invoker' context 'Invoker' do test 'should parse source and render as html5 article by default' do invoker = nil output = nil redirect_streams do |stdout, stderr| invoker = invoke_cli %w(-o -) output = stdout.string end assert !invoker.nil? doc = invoker.document assert !doc.nil? assert_equal 'Document Title', doc.doctitle assert_equal 'Doc Writer', doc.attr('author') assert_equal 'html5', doc.attr('backend') assert_equal '.html', doc.attr('outfilesuffix') assert_equal 'article', doc.attr('doctype') assert doc.blocks? assert_equal :preamble, doc.blocks.first.context assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body[@class="article"]/*[@id="header"]/h1[text() = "Document Title"]', output, 1 end test 'should set implicit doc info attributes' do sample_filepath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample.asciidoc')) sample_filedir = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures')) invoker = invoke_cli_to_buffer %w(-o /dev/null), sample_filepath doc = invoker.document assert_equal 'sample', doc.attr('docname') assert_equal sample_filepath, doc.attr('docfile') assert_equal sample_filedir, doc.attr('docdir') assert doc.attr?('docdate') assert doc.attr?('doctime') assert doc.attr?('docdatetime') assert invoker.read_output.empty? end test 'should accept document from stdin and write to stdout' do invoker = invoke_cli_to_buffer(%w(-s), '-') { 'content' } doc = invoker.document assert !doc.attr?('docname') assert !doc.attr?('docfile') assert_equal Dir.pwd, doc.attr('docdir') assert_equal doc.attr('docdate'), doc.attr('localdate') assert_equal doc.attr('doctime'), doc.attr('localtime') assert_equal doc.attr('docdatetime'), doc.attr('localdatetime') assert !doc.attr?('outfile') output = invoker.read_output assert !output.empty? assert_xpath '/*[@class="paragraph"]/p[text()="content"]', output, 1 end test 'should accept document from stdin and write to output file' do sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html')) begin invoker = invoke_cli(%W(-s -o #{sample_outpath}), '-') { 'content' } doc = invoker.document assert !doc.attr?('docname') assert !doc.attr?('docfile') assert_equal Dir.pwd, doc.attr('docdir') assert_equal doc.attr('docdate'), doc.attr('localdate') assert_equal doc.attr('doctime'), doc.attr('localtime') assert_equal doc.attr('docdatetime'), doc.attr('localdatetime') assert doc.attr?('outfile') assert_equal sample_outpath, doc.attr('outfile') assert File.exist?(sample_outpath) ensure FileUtils::rm_f(sample_outpath) end end test 'should allow docdir to be specified when input is a string' do expected_docdir = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures')) invoker = invoke_cli_to_buffer(%w(-s --base-dir test/fixtures -o /dev/null), '-') { 'content' } doc = invoker.document assert_equal expected_docdir, doc.attr('docdir') assert_equal expected_docdir, doc.base_dir end test 'should display version and exit' do redirect_streams do |stdout, stderr| invoke_cli %w(--version) assert_equal "Asciidoctor #{Asciidoctor::VERSION} [http://asciidoctor.org]", stdout.string.chomp end end test 'should report usage if no input file given' do redirect_streams do |stdout, stderr| invoke_cli [], nil assert_match(/Usage:/, stderr.string) end end test 'should report error if input file does not exist' do redirect_streams do |stdout, stderr| invoker = invoke_cli [], 'missing_file.asciidoc' assert_match(/input file .* missing/, stderr.string) assert_equal 1, invoker.code end end test 'should treat extra arguments as files' do redirect_streams do |stdout, stderr| invoker = invoke_cli %w(-o /dev/null extra arguments sample.asciidoc), nil assert_match(/input file .* missing/, stderr.string) assert_equal 1, invoker.code end end test 'should output to file name based on input file name' do sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample.html')) begin invoker = invoke_cli doc = invoker.document assert_equal sample_outpath, doc.attr('outfile') assert File.exist?(sample_outpath) output = File.read(sample_outpath) assert !output.empty? assert_xpath '/html', output, 1 assert_xpath '/html/head', output, 1 assert_xpath '/html/body', output, 1 assert_xpath '/html/head/title[text() = "Document Title"]', output, 1 assert_xpath '/html/body/*[@id="header"]/h1[text() = "Document Title"]', output, 1 ensure FileUtils::rm_f(sample_outpath) end end test 'should output to file in destination directory if set' do destination_path = File.expand_path(File.join(File.dirname(__FILE__), 'test_output')) sample_outpath = File.join(destination_path, 'sample.html') begin FileUtils::mkdir_p(destination_path) # QUESTION should -D be relative to working directory or source directory? invoker = invoke_cli %w(-D test/test_output) #invoker = invoke_cli %w(-D ../../test/test_output) doc = invoker.document assert_equal sample_outpath, doc.attr('outfile') assert File.exist?(sample_outpath) ensure FileUtils::rm_f(sample_outpath) FileUtils::rmdir(destination_path) end end test 'should output to file specified' do sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html')) begin invoker = invoke_cli %W(-o #{sample_outpath}) doc = invoker.document assert_equal sample_outpath, doc.attr('outfile') assert File.exist?(sample_outpath) ensure FileUtils::rm_f(sample_outpath) end end test 'should copy default css to target directory if linkcss is specified' do sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html')) asciidoctor_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor.css')) coderay_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor-coderay.css')) begin invoker = invoke_cli %W(-o #{sample_outpath} -a linkcss -a source-highlighter=coderay) invoker.document assert File.exist?(sample_outpath) assert File.exist?(asciidoctor_stylesheet) assert File.exist?(coderay_stylesheet) ensure FileUtils::rm_f(sample_outpath) FileUtils::rm_f(asciidoctor_stylesheet) FileUtils::rm_f(coderay_stylesheet) end end test 'should not copy default css to target directory if linkcss is set and copycss is unset' do sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html')) default_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor.css')) begin invoker = invoke_cli %W(-o #{sample_outpath} -a linkcss -a copycss!) invoker.document assert File.exist?(sample_outpath) assert !File.exist?(default_stylesheet) ensure FileUtils::rm_f(sample_outpath) end end test 'should render all passed files' do basic_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'basic.html')) sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample.html')) begin invoke_cli_with_filenames %w(), %w(basic.asciidoc sample.asciidoc) assert File.exist?(basic_outpath) assert File.exist?(sample_outpath) ensure FileUtils::rm_f(basic_outpath) FileUtils::rm_f(sample_outpath) end end test 'should render all files that matches a glob expression' do basic_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'basic.html')) begin invoke_cli_to_buffer %w(), "ba*.asciidoc" assert File.exist?(basic_outpath) ensure FileUtils::rm_f(basic_outpath) end end test 'should suppress header footer if specified' do invoker = invoke_cli_to_buffer %w(-s -o -) output = invoker.read_output assert_xpath '/html', output, 0 assert_xpath '/*[@id="preamble"]', output, 1 end test 'should not compact output by default' do # NOTE we are relying on the fact that the template leaves blank lines # this will always fail when using a template engine which strips blank lines by default invoker = invoke_cli_to_buffer(%w(-o -), '-') { '* content' } output = invoker.read_output assert_match(/\n[[:blank:]]*\n/, output) end test 'should compact output if specified' do # NOTE we are relying on the fact that the template leaves blank lines # this will always succeed when using a template engine which strips blank lines by default invoker = invoke_cli_to_buffer(%w(-C -s -o -), '-') { '* content' } output = invoker.read_output assert_no_match(/\n[[:blank:]]*\n/, output) end test 'should output a trailing endline to stdout' do invoker = nil output = nil redirect_streams do |stdout, stderr| invoker = invoke_cli %w(-o -) output = stdout.string end assert !invoker.nil? assert !output.nil? assert output.end_with?("\n") end test 'should set backend to html5 if specified' do invoker = invoke_cli_to_buffer %w(-b html5 -o -) doc = invoker.document assert_equal 'html5', doc.attr('backend') assert_equal '.html', doc.attr('outfilesuffix') output = invoker.read_output assert_xpath '/html', output, 1 end test 'should set backend to docbook45 if specified' do invoker = invoke_cli_to_buffer %w(-b docbook45 -o -) doc = invoker.document assert_equal 'docbook45', doc.attr('backend') assert_equal '.xml', doc.attr('outfilesuffix') output = invoker.read_output assert_xpath '/xmlns:article', output, 1 end test 'should set doctype to article if specified' do invoker = invoke_cli_to_buffer %w(-d article -o -) doc = invoker.document assert_equal 'article', doc.attr('doctype') output = invoker.read_output assert_xpath '/html/body[@class="article"]', output, 1 end test 'should set doctype to book if specified' do invoker = invoke_cli_to_buffer %w(-d book -o -) doc = invoker.document assert_equal 'book', doc.attr('doctype') output = invoker.read_output assert_xpath '/html/body[@class="book"]', output, 1 end test 'should locate custom templates based on template dir, template engine and backend' do custom_backend_root = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends')) invoker = invoke_cli_to_buffer %W(-E haml -T #{custom_backend_root} -o -) doc = invoker.document assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate end test 'should load custom templates from multiple template directories' do custom_backend_1 = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends/haml/html5')) custom_backend_2 = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends/haml/html5-tweaks')) invoker = invoke_cli_to_buffer %W(-T #{custom_backend_1} -T #{custom_backend_2} -o - -s) output = invoker.read_output assert_css '.paragraph', output, 0 assert_css '#preamble > .sectionbody > p', output, 1 end test 'should set attribute with value' do invoker = invoke_cli_to_buffer %w(--trace -a idprefix=id -s -o -) doc = invoker.document assert_equal 'id', doc.attr('idprefix') output = invoker.read_output assert_xpath '//h2[@id="idsection_a"]', output, 1 end test 'should set attribute with value containing equal sign' do invoker = invoke_cli_to_buffer %w(--trace -a toc -a toc-title=t=o=c -o -) doc = invoker.document assert_equal 't=o=c', doc.attr('toc-title') output = invoker.read_output assert_xpath '//*[@id="toctitle"][text() = "t=o=c"]', output, 1 end test 'should set attribute with quoted value containing a space' do # emulating commandline arguments: --trace -a toc -a note-caption="Note to self:" -o - invoker = invoke_cli_to_buffer %w(--trace -a toc -a note-caption=Note\ to\ self: -o -) doc = invoker.document assert_equal 'Note to self:', doc.attr('note-caption') output = invoker.read_output assert_xpath %(//*[#{contains_class('admonitionblock')}]//*[@class='title'][text() = 'Note to self:']), output, 1 end test 'should not set attribute ending in @ if defined in document' do invoker = invoke_cli_to_buffer %w(--trace -a idprefix=id@ -s -o -) doc = invoker.document assert_equal 'id_', doc.attr('idprefix') output = invoker.read_output assert_xpath '//h2[@id="id_section_a"]', output, 1 end test 'should set attribute with no value' do invoker = invoke_cli_to_buffer %w(-a icons -s -o -) doc = invoker.document assert_equal '', doc.attr('icons') output = invoker.read_output assert_xpath '//*[@class="admonitionblock note"]//img[@alt="Note"]', output, 1 end test 'should unset attribute ending in bang' do invoker = invoke_cli_to_buffer %w(-a sectids! -s -o -) doc = invoker.document assert !doc.attr?('sectids') output = invoker.read_output # leave the count loose in case we add more sections assert_xpath '//h2[not(@id)]', output end test 'default mode for cli should be unsafe' do invoker = invoke_cli_to_buffer %w(-o /dev/null) doc = invoker.document assert_equal Asciidoctor::SafeMode::UNSAFE, doc.safe end test 'should set safe mode if specified' do invoker = invoke_cli_to_buffer %w(--safe -o /dev/null) doc = invoker.document assert_equal Asciidoctor::SafeMode::SAFE, doc.safe end test 'should set safe mode to specified level' do levels = { 'unsafe' => Asciidoctor::SafeMode::UNSAFE, 'safe' => Asciidoctor::SafeMode::SAFE, 'server' => Asciidoctor::SafeMode::SERVER, 'secure' => Asciidoctor::SafeMode::SECURE, } levels.each do |name, const| invoker = invoke_cli_to_buffer %W(-S #{name} -o /dev/null) doc = invoker.document assert_equal const, doc.safe end end test 'should set eRuby impl if specified' do invoker = invoke_cli_to_buffer %w(--eruby erubis -o /dev/null) doc = invoker.document assert_equal 'erubis', doc.instance_variable_get('@options')[:eruby] end test 'should force default external encoding to UTF-8' do executable = File.expand_path(File.join(File.dirname(__FILE__), '..', 'bin', 'asciidoctor')) input_path = fixture_path 'encoding.asciidoc' old_lang = ENV['LANG'] ENV['LANG'] = 'US-ASCII' begin # using open3 to work around a bug in JRuby process_manager.rb, # which tries to run a gsub on stdout prematurely breaking the test require 'open3' #cmd = "#{executable} -o - --trace #{input_path}" cmd = "#{File.join RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']} #{executable} -o - --trace #{input_path}" _, stdout, stderr = Open3.popen3 cmd stderr_lines = stderr.readlines puts stderr_lines.join unless stderr_lines.empty? assert stderr_lines.empty?, 'Command failed. Expected to receive a rendered document.' stdout_lines = stdout.readlines assert !stdout_lines.empty? if Asciidoctor::FORCE_ENCODING stdout_lines.each do |l| l.force_encoding Encoding::UTF_8 end end stdout_str = stdout_lines.join assert stdout_str.include?('Codierungen sind verrückt auf älteren Versionen von Ruby') ensure ENV['LANG'] = old_lang end end end asciidoctor-0.1.4/test/lexer_test.rb000066400000000000000000000520261221220517700175050ustar00rootroot00000000000000require 'test_helper' context "Lexer" do test "is_section_title?" do assert Asciidoctor::Lexer.is_section_title?('AsciiDoc Home Page', '==================') assert Asciidoctor::Lexer.is_section_title?('=== AsciiDoc Home Page') end test 'sanitize attribute name' do assert_equal 'foobar', Asciidoctor::Lexer.sanitize_attribute_name("Foo Bar") assert_equal 'foo', Asciidoctor::Lexer.sanitize_attribute_name("foo") assert_equal 'foo3-bar', Asciidoctor::Lexer.sanitize_attribute_name("Foo 3^ # - Bar[") end test "collect unnamed attribute" do attributes = {} line = 'quote' expected = {1 => 'quote'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute double-quoted" do attributes = {} line = '"quote"' expected = {1 => 'quote'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect empty unnamed attribute double-quoted" do attributes = {} line = '""' expected = {1 => ''} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute double-quoted containing escaped quote" do attributes = {} line = '"ba\"zaar"' expected = {1 => 'ba"zaar'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute single-quoted" do attributes = {} line = '\'quote\'' expected = {1 => 'quote'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect empty unnamed attribute single-quoted" do attributes = {} line = '\'\'' expected = {1 => ''} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute single-quoted containing escaped quote" do attributes = {} line = '\'ba\\\'zaar\'' expected = {1 => 'ba\'zaar'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute with dangling delimiter" do attributes = {} line = 'quote , ' expected = {1 => 'quote'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attribute in second position after empty attribute" do attributes = {} line = ', John Smith' expected = {1 => nil, 2 => 'John Smith'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect unnamed attributes" do attributes = {} line = "first, second one, third" expected = {1 => 'first', 2 => 'second one', 3 => 'third'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attribute" do attributes = {} line = 'foo=bar' expected = {'foo' => 'bar'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attribute double-quoted" do attributes = {} line = 'foo="bar"' expected = {'foo' => 'bar'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test 'collect named attribute with double-quoted empty value' do attributes = {} line = 'height=100,caption="",link="images/octocat.png"' expected = {'height' => '100', 'caption' => '', 'link' => 'images/octocat.png'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attribute single-quoted" do attributes = {} line = 'foo=\'bar\'' expected = {'foo' => 'bar'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test 'collect named attribute with single-quoted empty value' do attributes = {} line = "height=100,caption='',link='images/octocat.png'" expected = {'height' => '100', 'caption' => '', 'link' => 'images/octocat.png'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attributes unquoted" do attributes = {} line = "first=value, second=two, third=3" expected = {'first' => 'value', 'second' => 'two', 'third' => '3'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attributes quoted" do attributes = {} line = "first='value', second=\"value two\", third=three" expected = {'first' => 'value', 'second' => 'value two', 'third' => 'three'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect named attributes quoted containing non-semantic spaces" do attributes = {} line = " first = 'value', second =\"value two\" , third= three " expected = {'first' => 'value', 'second' => 'value two', 'third' => 'three'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect mixed named and unnamed attributes" do attributes = {} line = "first, second=\"value two\", third=three, Sherlock Holmes" expected = {1 => 'first', 'second' => 'value two', 'third' => 'three', 4 => 'Sherlock Holmes'} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect options attribute" do attributes = {} line = "quote, options='opt1,opt2 , opt3'" expected = {1 => 'quote', 'options' => 'opt1,opt2 , opt3', 'opt1-option' => '', 'opt2-option' => '', 'opt3-option' => ''} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect opts attribute as options" do attributes = {} line = "quote, opts='opt1,opt2 , opt3'" expected = {1 => 'quote', 'options' => 'opt1,opt2 , opt3', 'opt1-option' => '', 'opt2-option' => '', 'opt3-option' => ''} Asciidoctor::AttributeList.new(line).parse_into(attributes) assert_equal expected, attributes end test "collect and rekey unnamed attributes" do attributes = {} line = "first, second one, third, fourth" expected = {1 => 'first', 2 => 'second one', 3 => 'third', 4 => 'fourth', 'a' => 'first', 'b' => 'second one', 'c' => 'third'} Asciidoctor::AttributeList.new(line).parse_into(attributes, ['a', 'b', 'c']) assert_equal expected, attributes end test "rekey positional attributes" do attributes = {1 => 'source', 2 => 'java'} expected = {1 => 'source', 2 => 'java', 'style' => 'source', 'language' => 'java'} Asciidoctor::AttributeList.rekey(attributes, ['style', 'language', 'linenums']) assert_equal expected, attributes end test 'parse style attribute with id and role' do attributes = {1 => 'style#id.role'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_equal 'style', style assert_nil original_style assert_equal 'style', attributes['style'] assert_equal 'id', attributes['id'] assert_equal 'role', attributes['role'] assert_equal 'style#id.role', attributes[1] end test 'parse style attribute with style, role, id and option' do attributes = {1 => 'style.role#id%fragment'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_equal 'style', style assert_nil original_style assert_equal 'style', attributes['style'] assert_equal 'id', attributes['id'] assert_equal 'role', attributes['role'] assert_equal '', attributes['fragment-option'] assert_equal 'fragment', attributes['options'] assert_equal 'style.role#id%fragment', attributes[1] end test 'parse style attribute with style, id and multiple roles' do attributes = {1 => 'style#id.role1.role2'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_equal 'style', style assert_nil original_style assert_equal 'style', attributes['style'] assert_equal 'id', attributes['id'] assert_equal 'role1 role2', attributes['role'] assert_equal 'style#id.role1.role2', attributes[1] end test 'parse style attribute with style, multiple roles and id' do attributes = {1 => 'style.role1.role2#id'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_equal 'style', style assert_nil original_style assert_equal 'style', attributes['style'] assert_equal 'id', attributes['id'] assert_equal 'role1 role2', attributes['role'] assert_equal 'style.role1.role2#id', attributes[1] end test 'parse style attribute with positional and original style' do attributes = {1 => 'new_style', 'style' => 'original_style'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_equal 'new_style', style assert_equal 'original_style', original_style assert_equal 'new_style', attributes['style'] assert_equal 'new_style', attributes[1] end test 'parse style attribute with id and role only' do attributes = {1 => '#id.role'} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_nil style assert_nil original_style assert_equal 'id', attributes['id'] assert_equal 'role', attributes['role'] assert_equal '#id.role', attributes[1] end test 'parse empty style attribute' do attributes = {1 => nil} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_nil style assert_nil original_style assert_nil attributes['id'] assert_nil attributes['role'] assert_nil attributes[1] end test 'parse style attribute with option should preserve existing options' do attributes = {1 => '%header', 'options' => 'footer', 'footer-option' => ''} style, original_style = Asciidoctor::Lexer.parse_style_attribute(attributes) assert_nil style assert_nil original_style assert_equal 'header,footer', attributes['options'] assert_equal '', attributes['header-option'] assert_equal '', attributes['footer-option'] end test "parse author first" do metadata, = parse_header_metadata 'Stuart' assert_equal 5, metadata.size assert_equal 1, metadata['authorcount'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stuart', metadata['firstname'] assert_equal 'S', metadata['authorinitials'] end test "parse author first last" do metadata, = parse_header_metadata 'Yukihiro Matsumoto' assert_equal 6, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Yukihiro Matsumoto', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Yukihiro', metadata['firstname'] assert_equal 'Matsumoto', metadata['lastname'] assert_equal 'YM', metadata['authorinitials'] end test "parse author first middle last" do metadata, = parse_header_metadata 'David Heinemeier Hansson' assert_equal 7, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'David Heinemeier Hansson', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'David', metadata['firstname'] assert_equal 'Heinemeier', metadata['middlename'] assert_equal 'Hansson', metadata['lastname'] assert_equal 'DHH', metadata['authorinitials'] end test "parse author first middle last email" do metadata, = parse_header_metadata 'David Heinemeier Hansson ' assert_equal 8, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'David Heinemeier Hansson', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'David', metadata['firstname'] assert_equal 'Heinemeier', metadata['middlename'] assert_equal 'Hansson', metadata['lastname'] assert_equal 'rails@ruby-lang.org', metadata['email'] assert_equal 'DHH', metadata['authorinitials'] end test "parse author first email" do metadata, = parse_header_metadata 'Stuart ' assert_equal 6, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Stuart', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stuart', metadata['firstname'] assert_equal 'founder@asciidoc.org', metadata['email'] assert_equal 'S', metadata['authorinitials'] end test "parse author first last email" do metadata, = parse_header_metadata 'Stuart Rackham ' assert_equal 7, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Stuart Rackham', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stuart', metadata['firstname'] assert_equal 'Rackham', metadata['lastname'] assert_equal 'founder@asciidoc.org', metadata['email'] assert_equal 'SR', metadata['authorinitials'] end test "parse author with hyphen" do metadata, = parse_header_metadata 'Tim Berners-Lee ' assert_equal 7, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Tim Berners-Lee', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Tim', metadata['firstname'] assert_equal 'Berners-Lee', metadata['lastname'] assert_equal 'founder@www.org', metadata['email'] assert_equal 'TB', metadata['authorinitials'] end test "parse author with single quote" do metadata, = parse_header_metadata 'Stephen O\'Grady ' assert_equal 7, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Stephen O\'Grady', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stephen', metadata['firstname'] assert_equal 'O\'Grady', metadata['lastname'] assert_equal 'founder@redmonk.com', metadata['email'] assert_equal 'SO', metadata['authorinitials'] end test "parse author with dotted initial" do metadata, = parse_header_metadata 'Heiko W. Rupp ' assert_equal 8, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Heiko W. Rupp', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Heiko', metadata['firstname'] assert_equal 'W.', metadata['middlename'] assert_equal 'Rupp', metadata['lastname'] assert_equal 'hwr@example.de', metadata['email'] assert_equal 'HWR', metadata['authorinitials'] end test "parse author with underscore" do metadata, = parse_header_metadata 'Tim_E Fella' assert_equal 6, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Tim E Fella', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Tim E', metadata['firstname'] assert_equal 'Fella', metadata['lastname'] assert_equal 'TF', metadata['authorinitials'] end test "parse author condenses whitespace" do metadata, = parse_header_metadata ' Stuart Rackham ' assert_equal 7, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Stuart Rackham', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stuart', metadata['firstname'] assert_equal 'Rackham', metadata['lastname'] assert_equal 'founder@asciidoc.org', metadata['email'] assert_equal 'SR', metadata['authorinitials'] end test "parse invalid author line becomes author" do metadata, = parse_header_metadata ' Stuart Rackham, founder of AsciiDoc ' assert_equal 5, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Stuart Rackham, founder of AsciiDoc ', metadata['author'] assert_equal metadata['author'], metadata['authors'] assert_equal 'Stuart Rackham, founder of AsciiDoc ', metadata['firstname'] assert_equal 'S', metadata['authorinitials'] end test 'parse multiple authors' do metadata, = parse_header_metadata 'Doc Writer ; John Smith ' assert_equal 2, metadata['authorcount'] assert_equal 'Doc Writer, John Smith', metadata['authors'] assert_equal 'Doc Writer', metadata['author'] assert_equal 'Doc Writer', metadata['author_1'] assert_equal 'John Smith', metadata['author_2'] end test "parse rev number date remark" do metadata, = parse_header_metadata "Ryan Waldron\nv0.0.7, 2013-12-18: The first release you can stand on" assert_equal 9, metadata.size assert_equal '0.0.7', metadata['revnumber'] assert_equal '2013-12-18', metadata['revdate'] assert_equal 'The first release you can stand on', metadata['revremark'] end test "parse rev date" do metadata, = parse_header_metadata "Ryan Waldron\n2013-12-18" assert_equal 7, metadata.size assert_equal '2013-12-18', metadata['revdate'] end # while compliant w/ AsciiDoc, this is just sloppy parsing test "treats arbitrary text on rev line as revdate" do metadata, = parse_header_metadata "Ryan Waldron\nfoobar\n" assert_equal 7, metadata.size assert_equal 'foobar', metadata['revdate'] end test "parse rev date remark" do metadata, = parse_header_metadata "Ryan Waldron\n2013-12-18: The first release you can stand on" assert_equal 8, metadata.size assert_equal '2013-12-18', metadata['revdate'] assert_equal 'The first release you can stand on', metadata['revremark'] end test "should not mistake attribute entry as rev remark" do metadata, = parse_header_metadata "Joe Cool\n:layout: post\n" assert_not_equal 'layout: post', metadata['revremark'] assert !metadata.has_key?('revdate') end test "parse rev remark only" do metadata, = parse_header_metadata "Joe Cool\n :Must start line with space\n" assert_equal 'Must start line with space', metadata['revremark'] assert_equal '', metadata['revdate'] end test "skip line comments before author" do metadata, = parse_header_metadata "// Asciidoctor\n// release artist\nRyan Waldron" assert_equal 6, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Ryan Waldron', metadata['author'] assert_equal 'Ryan', metadata['firstname'] assert_equal 'Waldron', metadata['lastname'] assert_equal 'RW', metadata['authorinitials'] end test "skip block comment before author" do metadata, = parse_header_metadata "////\nAsciidoctor\nrelease artist\n////\nRyan Waldron" assert_equal 6, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Ryan Waldron', metadata['author'] assert_equal 'Ryan', metadata['firstname'] assert_equal 'Waldron', metadata['lastname'] assert_equal 'RW', metadata['authorinitials'] end test "skip block comment before rev" do metadata, = parse_header_metadata "Ryan Waldron\n////\nAsciidoctor\nrelease info\n////\nv0.0.7, 2013-12-18" assert_equal 8, metadata.size assert_equal 1, metadata['authorcount'] assert_equal 'Ryan Waldron', metadata['author'] assert_equal '0.0.7', metadata['revnumber'] assert_equal '2013-12-18', metadata['revdate'] end test "attribute entry overrides generated author initials" do blankdoc = Asciidoctor::Document.new reader = Asciidoctor::Reader.new "Stuart Rackham \n:Author Initials: SJR".lines.entries metadata = Asciidoctor::Lexer.parse_header_metadata(reader, blankdoc) assert_equal 'SR', metadata['authorinitials'] assert_equal 'SJR', blankdoc.attributes['authorinitials'] end test 'reset block indent to 0' do input = <<-EOS def names @name.split ' ' end EOS expected = <<-EOS def names @name.split ' ' end EOS lines = input.lines.entries Asciidoctor::Lexer.reset_block_indent! lines assert_equal expected, lines.join end test 'reset block indent mixed with tabs and spaces to 0' do input = <<-EOS def names \t @name.split ' ' end EOS expected = <<-EOS def names @name.split ' ' end EOS lines = input.lines.entries Asciidoctor::Lexer.reset_block_indent! lines assert_equal expected, lines.join end test 'reset block indent to non-zero' do input = <<-EOS def names @name.split ' ' end EOS expected = <<-EOS def names @name.split ' ' end EOS lines = input.lines.entries Asciidoctor::Lexer.reset_block_indent! lines, 2 assert_equal expected, lines.join end test 'preserve block indent' do input = <<-EOS def names @name.split ' ' end EOS expected = input lines = input.lines.entries Asciidoctor::Lexer.reset_block_indent! lines, nil assert_equal expected, lines.join end test 'reset block indent hands empty lines gracefully' do input = [] expected = input lines = input.dup Asciidoctor::Lexer.reset_block_indent! lines assert_equal expected, lines end end asciidoctor-0.1.4/test/links_test.rb000066400000000000000000000263461221220517700175140ustar00rootroot00000000000000require 'test_helper' context 'Links' do test 'qualified url inline with text' do assert_xpath "//a[@href='http://asciidoc.org'][text() = 'http://asciidoc.org']", render_string("The AsciiDoc project is located at http://asciidoc.org.") end test 'qualified url with label' do assert_xpath "//a[@href='http://asciidoc.org'][text() = 'AsciiDoc']", render_string("We're parsing http://asciidoc.org[AsciiDoc] markup") end test 'qualified url with label containing escaped right square bracket' do assert_xpath "//a[@href='http://asciidoc.org'][text() = '[Ascii]Doc']", render_string("We're parsing http://asciidoc.org[[Ascii\\]Doc] markup") end test 'qualified url with label using link macro' do assert_xpath "//a[@href='http://asciidoc.org'][text() = 'AsciiDoc']", render_string("We're parsing link:http://asciidoc.org[AsciiDoc] markup") end test 'qualified url using macro syntax with multi-line label inline with text' do assert_xpath %{//a[@href='http://asciidoc.org'][text() = 'AsciiDoc\nmarkup']}, render_string("We're parsing link:http://asciidoc.org[AsciiDoc\nmarkup]") end test 'qualified url surrounded by angled brackets' do assert_xpath '//a[@href="http://asciidoc.org"][text()="http://asciidoc.org"]', render_string(' is the project page for AsciiDoc.'), 1 end test 'qualified url surrounded by round brackets' do assert_xpath '//a[@href="http://asciidoc.org"][text()="http://asciidoc.org"]', render_string('(http://asciidoc.org) is the project page for AsciiDoc.'), 1 end test 'qualified url containing round brackets' do assert_xpath '//a[@href="http://jruby.org/apidocs/org/jruby/Ruby.html#addModule(org.jruby.RubyModule)"][text()="addModule() adds a Ruby module"]', render_string('http://jruby.org/apidocs/org/jruby/Ruby.html#addModule(org.jruby.RubyModule)[addModule() adds a Ruby module]'), 1 end test 'qualified url adjacent to text in square brackets' do assert_xpath '//a[@href="http://asciidoc.org"][text()="AsciiDoc"]', render_string(']http://asciidoc.org[AsciiDoc] project page.'), 1 end test 'qualified url adjacent to text in round brackets' do assert_xpath '//a[@href="http://asciidoc.org"][text()="AsciiDoc"]', render_string(')http://asciidoc.org[AsciiDoc] project page.'), 1 end test 'qualified url using invalid link macro should not create link' do assert_xpath '//a', render_string('link:http://asciidoc.org is the project page for AsciiDoc.'), 0 end test 'escaped inline qualified url should not create link' do assert_xpath '//a', render_string('\http://asciidoc.org is the project page for AsciiDoc.'), 0 end test 'escaped inline qualified url using macro syntax should not create link' do assert_xpath '//a', render_string('\http://asciidoc.org[AsciiDoc] is the key to good docs.'), 0 end test 'inline qualified url followed by an endline should not include endline in link' do assert_xpath '//a[@href="http://github.com/asciidoctor"]', render_string("The source code for Asciidoctor can be found at http://github.com/asciidoctor\nwhich is a GitHub organization."), 1 end test 'qualified url divided by endline using macro syntax should not create link' do assert_xpath '//a', render_string("The source code for Asciidoctor can be found at link:http://github.com/asciidoctor\n[]which is a GitHub organization."), 0 end test 'qualified url containing whitespace using macro syntax should not create link' do assert_xpath '//a', render_string('I often need to refer to the chapter on link:http://asciidoc.org?q=attribute references[Attribute References].'), 0 end test 'qualified url containing an encoded space using macro syntax should create a link' do assert_xpath '//a', render_string('I often need to refer to the chapter on link:http://asciidoc.org?q=attribute%20references[Attribute References].'), 1 end test 'inline quoted qualified url should not consume surrounding angled brackets' do assert_xpath '//a[@href="http://github.com/asciidoctor"]', render_string('Asciidoctor GitHub organization: <**http://github.com/asciidoctor**>'), 1 end test 'link with quoted text should not be separated into attributes when linkattrs is set' do assert_xpath '//a[@href="http://search.example.com"][text()="Google, Yahoo, Bing"]', render_embedded_string('http://search.example.com["Google, Yahoo, Bing"]', :attributes => {'linkattrs' => ''}), 1 end test 'role and window attributes on link are processed when linkattrs is set' do assert_xpath '//a[@href="http://google.com"][@class="external"][@target="_blank"]', render_embedded_string('http://google.com[Google, role="external", window="_blank"]', :attributes => {'linkattrs' => ''}), 1 end test 'link text that ends in ^ should set link window to _blank' do assert_xpath '//a[@href="http://google.com"][@target="_blank"]', render_embedded_string('http://google.com[Google^]'), 1 end test 'inline irc link' do assert_xpath '//a[@href="irc://irc.freenode.net"][text()="irc://irc.freenode.net"]', render_embedded_string('irc://irc.freenode.net'), 1 end test 'inline irc link with text' do assert_xpath '//a[@href="irc://irc.freenode.net"][text()="Freenode IRC"]', render_embedded_string('irc://irc.freenode.net[Freenode IRC]'), 1 end test 'inline ref' do doc = document_from_string 'Here you can read about tigers.[[tigers]]' output = doc.render assert_equal '[tigers]', doc.references[:ids]['tigers'] assert_xpath '//a[@id = "tigers"]', output, 1 assert_xpath '//a[@id = "tigers"]/child::text()', output, 0 end test 'inline ref with reftext' do doc = document_from_string 'Here you can read about tigers.[[tigers,Tigers]]' output = doc.render assert_equal 'Tigers', doc.references[:ids]['tigers'] assert_xpath '//a[@id = "tigers"]', output, 1 assert_xpath '//a[@id = "tigers"]/child::text()', output, 0 end test 'escaped inline ref' do doc = document_from_string 'Here you can read about tigers.\[[tigers]]' output = doc.render assert !doc.references[:ids].has_key?('tigers') assert_xpath '//a[@id = "tigers"]', output, 0 end test 'xref using angled bracket syntax' do doc = document_from_string '<>' doc.references[:ids]['tigers'] = '[tigers]' assert_xpath '//a[@href="#tigers"][text() = "[tigers]"]', doc.render, 1 end test 'xref using angled bracket syntax with label' do assert_xpath '//a[@href="#tigers"][text() = "About Tigers"]', render_string('<>'), 1 end test 'xref using angled bracket syntax with quoted label' do assert_xpath '//a[@href="#tigers"][text() = "About Tigers"]', render_string('<>'), 1 end test 'xref using angled bracket syntax with path sans extension' do doc = document_from_string '<>', :header_footer => false assert_xpath '//a[@href="tigers.html"][text() = "[tigers]"]', doc.render, 1 end test 'xref using angled bracket syntax with path and extension' do doc = document_from_string '<>', :header_footer => false assert_xpath '//a[@href="tigers.html"][text() = "[tigers]"]', doc.render, 1 end test 'xref using angled bracket syntax with path and fragment' do doc = document_from_string '<>', :header_footer => false assert_xpath '//a[@href="tigers.html#about"][text() = "[tigers#about]"]', doc.render, 1 end test 'xref using angled bracket syntax with path, fragment and text' do doc = document_from_string '<>', :header_footer => false assert_xpath '//a[@href="tigers.html#about"][text() = "About Tigers"]', doc.render, 1 end test 'xref using angled bracket syntax with path which has been included in this document' do doc = document_from_string '<>', :header_footer => false doc.references[:includes] << 'tigers' assert_xpath '//a[@href="#about"][text() = "About Tigers"]', doc.render, 1 end test 'xref using angled bracket syntax with nested path which has been included in this document' do doc = document_from_string '<>', :header_footer => false doc.references[:includes] << 'part1/tigers' assert_xpath '//a[@href="#about"][text() = "About Tigers"]', doc.render, 1 end test 'xref using angled bracket syntax inline with text' do assert_xpath '//a[@href="#tigers"][text() = "about tigers"]', render_string('Want to learn <>?'), 1 end test 'xref using angled bracket syntax with multi-line label inline with text' do assert_xpath %{//a[@href="#tigers"][normalize-space(text()) = "about tigers"]}, render_string("Want to learn <>?"), 1 end test 'xref with escaped text' do # when \x0 was used as boundary character for passthrough, it was getting stripped # now using \e as boundary character, which resolves issue input = 'See the <> section for data about tigers' output = render_embedded_string input assert_xpath %(//a[@href="#tigers"]/code[text()="[tigers]"]), output, 1 end test 'xref using macro syntax' do doc = document_from_string 'xref:tigers[]' doc.references[:ids]['tigers'] = '[tigers]' assert_xpath '//a[@href="#tigers"][text() = "[tigers]"]', doc.render, 1 end test 'xref using macro syntax with label' do assert_xpath '//a[@href="#tigers"][text() = "About Tigers"]', render_string('xref:tigers[About Tigers]'), 1 end test 'xref using macro syntax inline with text' do assert_xpath '//a[@href="#tigers"][text() = "about tigers"]', render_string('Want to learn xref:tigers[about tigers]?'), 1 end test 'xref using macro syntax with multi-line label inline with text' do assert_xpath %{//a[@href="#tigers"][normalize-space(text()) = "about tigers"]}, render_string("Want to learn xref:tigers[about\ntigers]?"), 1 end test 'xref using invalid macro syntax does not create link' do doc = document_from_string 'xref:tigers' doc.references[:ids]['tigers'] = '[tigers]' assert_xpath '//a', doc.render, 0 end test 'xref creates link for unknown reference' do doc = document_from_string '<>' assert_xpath '//a[@href="#tigers"][text() = "[tigers]"]', doc.render, 1 end test 'xref shows label from title of target for forward and backward references in html backend' do input = <<-EOS == Section A <\<_section_b>> == Section B <\<_section_a>> EOS output = render_embedded_string input assert_xpath '//h2[@id="_section_a"][text()="Section A"]', output, 1 assert_xpath '//a[@href="#_section_a"][text()="Section A"]', output, 1 assert_xpath '//h2[@id="_section_b"][text()="Section B"]', output, 1 assert_xpath '//a[@href="#_section_b"][text()="Section B"]', output, 1 end test 'anchor creates reference' do doc = document_from_string "[[tigers]]Tigers roam here." assert_equal({'tigers' => '[tigers]'}, doc.references[:ids]) end test 'anchor with label creates reference' do doc = document_from_string "[[tigers,Tigers]]Tigers roam here." assert_equal({'tigers' => 'Tigers'}, doc.references[:ids]) end test 'anchor with quoted label creates reference' do doc = document_from_string %([["tigers","Tigers roam here"]]Tigers roam here.) assert_equal({'tigers' => "Tigers roam here"}, doc.references[:ids]) end end asciidoctor-0.1.4/test/lists_test.rb000066400000000000000000003563671221220517700175430ustar00rootroot00000000000000require 'test_helper' context "Bulleted lists (:ulist)" do context "Simple lists" do test "dash elements with no blank lines" do input = <<-EOS List ==== - Foo - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'indented dash elements using spaces' do input = <<-EOS - Foo - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'indented dash elements using tabs' do input = <<-EOS \t-\tFoo \t-\tBoo \t-\tBlech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test "dash elements separated by blank lines should merge lists" do input = <<-EOS List ==== - Foo - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'dash elements with interspersed line comments should be skipped and not break list' do input = <<-EOS == List - Foo // line comment // another line comment - Boo // line comment more text // another line comment - Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath %((//ul/li)[2]/p[text()="Boo\nmore text"]), output, 1 end test "dash elements separated by a line comment offset by blank lines should not merge lists" do input = <<-EOS List ==== - Foo - Boo // - Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 end test "dash elements separated by a block title offset by a blank line should not merge lists" do input = <<-EOS List ==== - Foo - Boo .Also - Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 assert_xpath '(//ul)[2]/preceding-sibling::*[@class = "title"][text() = "Also"]', output, 1 end test "dash elements separated by an attribute entry offset by a blank line should not merge lists" do input = <<-EOS == List - Foo - Boo :foo: bar - Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 end test 'a non-indented wrapped line is folded into text of list item' do input = <<-EOS List ==== - Foo wrapped content - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li[1]/*', output, 1 assert_xpath "//ul/li[1]/p[text() = 'Foo\nwrapped content']", output, 1 end test 'a non-indented wrapped line that resembles a block title is folded into text of list item' do input = <<-EOS == List - Foo .wrapped content - Boo - Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li[1]/*', output, 1 assert_xpath "//ul/li[1]/p[text() = 'Foo\n.wrapped content']", output, 1 end test 'a non-indented wrapped line that resembles an attribute entry is folded into text of list item' do input = <<-EOS == List - Foo :foo: bar - Boo - Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li[1]/*', output, 1 assert_xpath "//ul/li[1]/p[text() = 'Foo\n:foo: bar']", output, 1 end test 'a list item with a nested marker terminates non-indented paragraph for text of list item' do input = <<-EOS - Foo Bar * Foo EOS output = render_embedded_string input assert_css 'ul ul', output, 1 assert !output.include?('* Foo') end test 'a list item for a different list terminates non-indented paragraph for text of list item' do input = <<-EOS == Example 1 - Foo Bar . Foo == Example 2 * Item text term:: def EOS output = render_embedded_string input assert_css 'ul ol', output, 1 assert !output.include?('* Foo') assert_css 'ul dl', output, 1 assert !output.include?('term:: def') end test 'an indented wrapped line is unindented and folded into text of list item' do input = <<-EOS List ==== - Foo wrapped content - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li[1]/*', output, 1 assert_xpath "//ul/li[1]/p[text() = 'Foo\nwrapped content']", output, 1 end test 'wrapped list item with hanging indent followed by non-indented line' do input = <<-EOS == Lists - list item 1 // not line comment second wrapped line - list item 2 EOS output = render_embedded_string input assert_css 'ul', output, 1 assert_css 'ul li', output, 2 # NOTE for some reason, we're getting an extra line after the indented line lines = xmlnodes_at_xpath('(//ul/li)[1]/p', output, 1).text.gsub(/\n[[:space:]]*\n/, "\n").lines.entries assert_equal 3, lines.size assert_equal 'list item 1', lines[0].chomp assert_equal ' // not line comment', lines[1].chomp assert_equal 'second wrapped line', lines[2].chomp end test 'a list item with a nested marker terminates indented paragraph for text of list item' do input = <<-EOS - Foo Bar * Foo EOS output = render_embedded_string input assert_css 'ul ul', output, 1 assert !output.include?('* Foo') end test 'a list item for a different list terminates indented paragraph for text of list item' do input = <<-EOS == Example 1 - Foo Bar . Foo == Example 2 * Item text term:: def EOS output = render_embedded_string input assert_css 'ul ol', output, 1 assert !output.include?('* Foo') assert_css 'ul dl', output, 1 assert !output.include?('term:: def') end test "a literal paragraph offset by blank lines in list content is appended as a literal block" do input = <<-EOS List ==== - Foo literal - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul/li)[1]/p[text() = "Foo"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '((//ul/li)[1]/*[@class="literalblock"])[1]//pre[text() = "literal"]', output, 1 end test "a literal paragraph offset by a blank line in list content followed by line with continuation is appended as two blocks" do input = <<-EOS List ==== - Foo literal + para - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul/li)[1]/p[text() = "Foo"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '((//ul/li)[1]/*[@class="literalblock"])[1]//pre[text() = "literal"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'an admonition paragraph attached by a line continuation to a list item with wrapped text should produce admonition' do input = <<-EOS - first-line text wrapped text + NOTE: This is a note. EOS output = render_embedded_string input assert_css 'ul', output, 1 assert_css 'ul > li', output, 1 assert_css 'ul > li > p', output, 1 assert_xpath %(//ul/li/p[text()="first-line text\nwrapped text"]), output, 1 assert_css 'ul > li > p + .admonitionblock.note', output, 1 assert_xpath '//ul/li/*[@class="admonitionblock note"]//td[@class="content"][normalize-space(text())="This is a note."]', output, 1 end test 'appends line as paragraph if attached by continuation following line comment' do input = <<-EOS - list item 1 // line comment + paragraph in list item 1 - list item 2 EOS output = render_embedded_string input assert_css 'ul', output, 1 assert_css 'ul li', output, 2 assert_xpath '(//ul/li)[1]/p[text()="list item 1"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="paragraph"]/p[text()="paragraph in list item 1"]', output, 1 assert_xpath '(//ul/li)[2]/p[text()="list item 2"]', output, 1 end test "a literal paragraph with a line that appears as a list item that is followed by a continuation should create two blocks" do input = <<-EOS * Foo + literal . still literal + para * Bar EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '(//ul/li)[1]/p[text() = "Foo"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath %(((//ul/li)[1]/*[@class="literalblock"])[1]//pre[text() = " literal\n. still literal"]), output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test "consecutive literal paragraph offset by blank lines in list content are appended as a literal blocks" do input = <<-EOS List ==== - Foo literal more literal - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul/li)[1]/p[text() = "Foo"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]', output, 2 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="literalblock"]', output, 2 assert_xpath '((//ul/li)[1]/*[@class="literalblock"])[1]//pre[text() = "literal"]', output, 1 assert_xpath "((//ul/li)[1]/*[@class='literalblock'])[2]//pre[text() = 'more\nliteral']", output, 1 end test "a literal paragraph without a trailing blank line consumes following list items" do input = <<-EOS List ==== - Foo literal - Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 1 assert_xpath '(//ul/li)[1]/p[text() = "Foo"]', output, 1 assert_xpath '(//ul/li)[1]/*[@class="literalblock"]', output, 1 assert_xpath '(//ul/li)[1]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath "((//ul/li)[1]/*[@class='literalblock'])[1]//pre[text() = ' literal\n- Boo\n- Blech']", output, 1 end test "asterisk elements with no blank lines" do input = <<-EOS List ==== * Foo * Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'indented asterisk elements using spaces' do input = <<-EOS * Foo * Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'indented asterisk elements using tabs' do input = <<-EOS \t*\tFoo \t*\tBoo \t*\tBlech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'should represent block style as style class' do ['disc', 'square', 'circle'].each do |style| input = <<-EOS [#{style}] * a * b * c EOS output = render_embedded_string input assert_css ".ulist.#{style}", output, 1 assert_css ".ulist.#{style} ul.#{style}", output, 1 end end test "asterisk elements separated by blank lines should merge lists" do input = <<-EOS List ==== * Foo * Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 end test 'asterisk elements with interspersed line comments should be skipped and not break list' do input = <<-EOS == List * Foo // line comment // another line comment * Boo // line comment more text // another line comment * Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath %((//ul/li)[2]/p[text()="Boo\nmore text"]), output, 1 end test "asterisk elements separated by a line comment offset by blank lines should not merge lists" do input = <<-EOS List ==== * Foo * Boo // * Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 end test "asterisk elements separated by a block title offset by a blank line should not merge lists" do input = <<-EOS List ==== * Foo * Boo .Also * Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 assert_xpath '(//ul)[2]/preceding-sibling::*[@class = "title"][text() = "Also"]', output, 1 end test "asterisk elements separated by an attribute entry offset by a blank line should not merge lists" do input = <<-EOS == List * Foo * Boo :foo: bar * Blech EOS output = render_embedded_string input assert_xpath '//ul', output, 2 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[2]/li', output, 1 end test "list should terminate before next lower section heading" do input = <<-EOS List ==== * first item * second item == Section EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//h2[text() = "Section"]', output, 1 end test "list should terminate before next lower section heading with implicit id" do input = <<-EOS List ==== * first item * second item [[sec]] == Section EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//h2[@id = "sec"][text() = "Section"]', output, 1 end test 'should not find section title immediately below last list item' do input = <<-EOS * first * second == Not a section EOS output = render_embedded_string input assert_css 'ul', output, 1 assert_css 'ul > li', output, 2 assert_css 'h2', output, 0 assert output.include?('== Not a section') assert_xpath %((//li)[2]/p[text() = "second\n== Not a section"]), output, 1 end end context "Lists with inline markup" do test "quoted text" do input = <<-EOS List ==== - I am *strong*. - I am 'stressed'. - I am `flexible`. EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul/li)[1]//strong', output, 1 assert_xpath '(//ul/li)[2]//em', output, 1 assert_xpath '(//ul/li)[3]//code', output, 1 end test "attribute substitutions" do input = <<-EOS List ==== :foo: bar - side a {brvbar} side b - Take me to a {foo}. EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '(//ul/li)[1]//p[text() = "side a | side b"]', output, 1 assert_xpath '(//ul/li)[2]//p[text() = "Take me to a bar."]', output, 1 end test "leading dot is treated as text not block title" do input = <<-EOS * .first * .second * .third EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 3 %w(.first .second .third).each_with_index do |text, index| assert_xpath "(//ul/li)[#{index + 1}]//p[text() = '#{text}']", output, 1 end end test "word ending sentence on continuing line not treated as a list item" do input = <<-EOS A. This is the story about AsciiDoc. It begins here. B. And it ends here. EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 2 end end context "Nested lists" do test "asterisk element mixed with dash elements should be nested" do input = <<-EOS List ==== - Foo * Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[1]/li//ul/li', output, 1 end test "dash element mixed with asterisks elements should be nested" do input = <<-EOS List ==== * Foo - Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[1]/li//ul/li', output, 1 end test "lines prefixed with alternating list markers separated by blank lines should be nested" do input = <<-EOS List ==== - Foo * Boo - Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[1]/li//ul/li', output, 1 end test "nested elements (2) with asterisks" do input = <<-EOS List ==== * Foo ** Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 2 assert_xpath '//ul/li', output, 3 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '(//ul)[1]/li//ul/li', output, 1 end test "nested elements (3) with asterisks" do input = <<-EOS List ==== * Foo ** Boo *** Snoo * Blech EOS output = render_string input assert_xpath '//ul', output, 3 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '(((//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 end test "nested elements (4) with asterisks" do input = <<-EOS List ==== * Foo ** Boo *** Snoo **** Froo * Blech EOS output = render_string input assert_xpath '//ul', output, 4 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '(((//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '((((//ul)[1]/li//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 end test "nested elements (5) with asterisks" do input = <<-EOS List ==== * Foo ** Boo *** Snoo **** Froo ***** Groo * Blech EOS output = render_string input assert_xpath '//ul', output, 5 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '(((//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '((((//ul)[1]/li//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 assert_xpath '(((((//ul)[1]/li//ul)[1]/li//ul)[1]/li//ul)[1]/li//ul)[1]/li', output, 1 end test "nested ordered elements (2)" do input = <<-EOS List ==== . Foo .. Boo . Blech EOS output = render_string input assert_xpath '//ol', output, 2 assert_xpath '//ol/li', output, 3 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '(//ol)[1]/li//ol/li', output, 1 end test "nested ordered elements (3)" do input = <<-EOS List ==== . Foo .. Boo ... Snoo . Blech EOS output = render_string input assert_xpath '//ol', output, 3 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '((//ol)[1]/li//ol)[1]/li', output, 1 assert_xpath '(((//ol)[1]/li//ol)[1]/li//ol)[1]/li', output, 1 end test "nested unordered inside ordered elements" do input = <<-EOS List ==== . Foo * Boo . Blech EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ul', output, 1 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '((//ol)[1]/li//ul)[1]/li', output, 1 end test "nested ordered inside unordered elements" do input = <<-EOS List ==== * Foo . Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ol', output, 1 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ol)[1]/li', output, 1 end test 'three levels of alternating unordered and ordered elements' do input = <<-EOS == Lists * bullet 1 . numbered 1.1 ** bullet 1.1.1 * bullet 2 EOS output = render_embedded_string input assert_css '.ulist', output, 2 assert_css '.olist', output, 1 assert_css '.ulist > ul > li > p', output, 3 assert_css '.ulist > ul > li > p + .olist', output, 1 assert_css '.ulist > ul > li > p + .olist > ol > li > p', output, 1 assert_css '.ulist > ul > li > p + .olist > ol > li > p + .ulist', output, 1 assert_css '.ulist > ul > li > p + .olist > ol > li > p + .ulist > ul > li > p', output, 1 assert_css '.ulist > ul > li + li > p', output, 1 end test "lines with alternating markers of unordered and ordered list types separated by blank lines should be nested" do input = <<-EOS List ==== * Foo . Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ol', output, 1 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ol)[1]/li', output, 1 end test 'list item with literal content should not consume nested list of different type' do input = <<-EOS List ==== - bullet literal but not hungry . numbered EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//li', output, 2 assert_xpath '//ul//ol', output, 1 assert_xpath '//ul/li/p', output, 1 assert_xpath '//ul/li/p[text()="bullet"]', output, 1 assert_xpath '//ul/li/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath %(//ul/li/p/following-sibling::*[@class="literalblock"]//pre[text()="literal\nbut not\nhungry"]), output, 1 assert_xpath '//*[@class="literalblock"]/following-sibling::*[@class="olist arabic"]', output, 1 assert_xpath '//*[@class="literalblock"]/following-sibling::*[@class="olist arabic"]//p[text()="numbered"]', output, 1 end test 'nested list item does not eat the title of the following detached block' do input = <<-EOS List ==== - bullet * nested bullet 1 * nested bullet 2 .Title .... literal .... EOS output = render_embedded_string input assert_xpath '//*[@class="ulist"]/ul', output, 2 assert_xpath '(//*[@class="ulist"])[1]/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '(//*[@class="ulist"])[1]/following-sibling::*[@class="literalblock"]/*[@class="title"]', output, 1 end test "lines with alternating markers of bulleted and labeled list types separated by blank lines should be nested" do input = <<-EOS List ==== * Foo term1:: def1 * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//dl', output, 1 assert_xpath '//ul[1]/li', output, 2 assert_xpath '//ul[1]/li//dl[1]/dt', output, 1 assert_xpath '//ul[1]/li//dl[1]/dd', output, 1 end test "nested ordered with attribute inside unordered elements" do input = <<-EOS Blah ==== * Foo [start=2] . Boo * Blech EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ol', output, 1 assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li//ol)[1][@start = 2]/li', output, 1 end end context "List continuations" do test "adjacent list continuation line attaches following paragraph" do input = <<-EOS Lists ===== * Item one, paragraph one + Item one, paragraph two + * Item two EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '//ul/li[1]//p', output, 2 assert_xpath '//ul/li[1]/p[text() = "Item one, paragraph one"]', output, 1 assert_xpath '//ul/li[1]/*[@class = "paragraph"]/p[text() = "Item one, paragraph two"]', output, 1 end test "adjacent list continuation line attaches following block" do input = <<-EOS Lists ===== * Item one, paragraph one + .... Item one, literal block .... + * Item two EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@class = "literalblock"]', output, 1 end test "adjacent list continuation line attaches following block with block attributes" do input = <<-EOS Lists ===== * Item one, paragraph one + :foo: bar [[beck]] .Read the following aloud to yourself [source, ruby] ---- 5.times { print "Odelay!" } ---- * Item two EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"][@class = "listingblock"]', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"]/div[@class="title"][starts-with(text(),"Read")]', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@id="beck"]//code[@class="ruby language-ruby"][starts-with(text(),"5.times")]', output, 1 end test 'trailing block attribute line attached by continuation should not create block' do input = <<-EOS Lists ===== * Item one, paragraph one + [source] * Item two EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/*', output, 1 assert_xpath '//ul/li//*[@class="listingblock"]', output, 0 end test 'trailing block title line attached by continuation should not create block' do input = <<-EOS Lists ===== * Item one, paragraph one + .Disappears into the ether * Item two EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/*', output, 1 end test 'consecutive blocks in list continuation attach to list item' do input = <<-EOS Lists ===== * Item one, paragraph one + .... Item one, literal block .... + ____ Item one, quote block ____ + * Item two EOS output = render_embedded_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[1][@class = "literalblock"]', output, 1 assert_xpath '(//ul/li[1]/p/following-sibling::*)[2][@class = "quoteblock"]', output, 1 end test 'list item with hanging indent followed by block attached by list continuation' do input = <<-EOS == Lists . list item 1 continued + -- open block in list item 1 -- . list item 2 EOS output = render_embedded_string input assert_css 'ol', output, 1 assert_css 'ol li', output, 2 assert_xpath %((//ol/li)[1]/p[text()="list item 1\ncontinued"]), output, 1 assert_xpath '(//ol/li)[1]/p/following-sibling::*[@class="openblock"]', output, 1 assert_xpath '(//ol/li)[1]/p/following-sibling::*[@class="openblock"]//p[text()="open block in list item 1"]', output, 1 assert_xpath %((//ol/li)[2]/p[text()="list item 2"]), output, 1 end test 'list item paragraph in list item and nested list item' do input = <<-EOS == Lists . list item 1 + list item 1 paragraph * nested list item + nested list item paragraph . list item 2 EOS output = render_embedded_string input assert_css '.olist ol', output, 1 assert_css '.olist ol > li', output, 2 assert_css '.ulist ul', output, 1 assert_css '.ulist ul > li', output, 1 assert_xpath '(//ol/li)[1]/*', output, 3 assert_xpath '((//ol/li)[1]/*)[1]/self::p', output, 1 assert_xpath '((//ol/li)[1]/*)[1]/self::p[text()="list item 1"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="paragraph"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="ulist"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="ulist"]/ul/li', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="ulist"]/ul/li/p[text()="nested list item"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="ulist"]/ul/li/p/following-sibling::div[@class="paragraph"]', output, 1 end test 'trailing list continuations should attach to list items at respective levels' do input = <<-EOS == Lists . list item 1 + * nested list item 1 * nested list item 2 + paragraph for nested list item 2 + paragraph for list item 1 . list item 2 EOS output = render_embedded_string input assert_css '.olist ol', output, 1 assert_css '.olist ol > li', output, 2 assert_css '.ulist ul', output, 1 assert_css '.ulist ul > li', output, 2 assert_css '.olist .ulist', output, 1 assert_xpath '(//ol/li)[1]/*', output, 3 assert_xpath '((//ol/li)[1]/*)[1]/self::p', output, 1 assert_xpath '((//ol/li)[1]/*)[1]/self::p[text()="list item 1"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/div[@class="paragraph"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="paragraph"]', output, 1 end test 'trailing list continuations should attach to list items of different types at respective levels' do input = <<-EOS == Lists * bullet 1 . numbered 1.1 ** bullet 1.1.1 + numbered 1.1 paragraph + bullet 1 paragraph * bullet 2 EOS output = render_embedded_string input assert_xpath '(//ul)[1]/li', output, 2 assert_xpath '((//ul)[1]/li[1])/*', output, 3 assert_xpath '(((//ul)[1]/li[1])/*)[1]/self::p[text()="bullet 1"]', output, 1 assert_xpath '(((//ul)[1]/li[1])/*)[2]/ol', output, 1 assert_xpath '(((//ul)[1]/li[1])/*)[3]/self::div[@class="paragraph"]/p[text()="bullet 1 paragraph"]', output, 1 assert_xpath '((//ul)[1]/li)[1]/div/ol/li', output, 1 assert_xpath '((//ul)[1]/li)[1]/div/ol/li/*', output, 3 assert_xpath '(((//ul)[1]/li)[1]/div/ol/li/*)[1]/self::p[text()="numbered 1.1"]', output, 1 assert_xpath '(((//ul)[1]/li)[1]/div/ol/li/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '(((//ul)[1]/li)[1]/div/ol/li/*)[3]/self::div[@class="paragraph"]/p[text()="numbered 1.1 paragraph"]', output, 1 assert_xpath '((//ul)[1]/li)[1]/div/ol/li/div[@class="ulist"]/ul/li', output, 1 assert_xpath '((//ul)[1]/li)[1]/div/ol/li/div[@class="ulist"]/ul/li/*', output, 1 assert_xpath '((//ul)[1]/li)[1]/div/ol/li/div[@class="ulist"]/ul/li/p[text()="bullet 1.1.1"]', output, 1 end test 'repeated list continuations should attach to list items at respective levels' do input = <<-EOS == Lists . list item 1 * nested list item 1 + -- open block for nested list item 1 -- + * nested list item 2 + paragraph for nested list item 2 + paragraph for list item 1 . list item 2 EOS output = render_embedded_string input assert_css '.olist ol', output, 1 assert_css '.olist ol > li', output, 2 assert_css '.ulist ul', output, 1 assert_css '.ulist ul > li', output, 2 assert_css '.olist .ulist', output, 1 assert_xpath '(//ol/li)[1]/*', output, 3 assert_xpath '((//ol/li)[1]/*)[1]/self::p', output, 1 assert_xpath '((//ol/li)[1]/*)[1]/self::p[text()="list item 1"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/div[@class="openblock"]', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/div[@class="paragraph"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="paragraph"]', output, 1 end test 'repeated list continuations attached directly to list item should attach to list items at respective levels' do input = <<-EOS == Lists . list item 1 + * nested list item 1 + -- open block for nested list item 1 -- + * nested list item 2 + paragraph for nested list item 2 + paragraph for list item 1 . list item 2 EOS output = render_embedded_string input assert_css '.olist ol', output, 1 assert_css '.olist ol > li', output, 2 assert_css '.ulist ul', output, 1 assert_css '.ulist ul > li', output, 2 assert_css '.olist .ulist', output, 1 assert_xpath '(//ol/li)[1]/*', output, 3 assert_xpath '((//ol/li)[1]/*)[1]/self::p', output, 1 assert_xpath '((//ol/li)[1]/*)[1]/self::p[text()="list item 1"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/div[@class="openblock"]', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/div[@class="paragraph"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="paragraph"]', output, 1 end test 'repeated list continuations should attach to list items at respective levels ignoring blank lines' do input = <<-EOS == Lists . list item 1 + * nested list item 1 + -- open block for nested list item 1 -- + * nested list item 2 + paragraph for nested list item 2 + paragraph for list item 1 . list item 2 EOS output = render_embedded_string input assert_css '.olist ol', output, 1 assert_css '.olist ol > li', output, 2 assert_css '.ulist ul', output, 1 assert_css '.ulist ul > li', output, 2 assert_css '.olist .ulist', output, 1 assert_xpath '(//ol/li)[1]/*', output, 3 assert_xpath '((//ol/li)[1]/*)[1]/self::p', output, 1 assert_xpath '((//ol/li)[1]/*)[1]/self::p[text()="list item 1"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[1]/div[@class="openblock"]', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/*', output, 2 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/p', output, 1 assert_xpath '(((//ol/li)[1]/*)[2]/self::div[@class="ulist"]/ul/li)[2]/div[@class="paragraph"]', output, 1 assert_xpath '((//ol/li)[1]/*)[3]/self::div[@class="paragraph"]', output, 1 end test 'trailing list continuations should ignore preceding blank lines' do input = <<-EOS == Lists * bullet 1 ** bullet 1.1 *** bullet 1.1.1 + -- open block -- + bullet 1.1 paragraph + bullet 1 paragraph * bullet 2 EOS output = render_embedded_string input assert_xpath '((//ul)[1]/li[1])/*', output, 3 assert_xpath '(((//ul)[1]/li[1])/*)[1]/self::p[text()="bullet 1"]', output, 1 assert_xpath '(((//ul)[1]/li[1])/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '(((//ul)[1]/li[1])/*)[3]/self::div[@class="paragraph"]/p[text()="bullet 1 paragraph"]', output, 1 assert_xpath '((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li', output, 1 assert_xpath '((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/*', output, 3 assert_xpath '(((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/*)[1]/self::p[text()="bullet 1.1"]', output, 1 assert_xpath '(((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/*)[2]/self::div[@class="ulist"]', output, 1 assert_xpath '(((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/*)[3]/self::div[@class="paragraph"]/p[text()="bullet 1.1 paragraph"]', output, 1 assert_xpath '((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/div[@class="ulist"]/ul/li', output, 1 assert_xpath '((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/div[@class="ulist"]/ul/li/*', output, 2 assert_xpath '(((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/div[@class="ulist"]/ul/li/*)[1]/self::p', output, 1 assert_xpath '(((//ul)[1]/li)[1]/div[@class="ulist"]/ul/li/div[@class="ulist"]/ul/li/*)[2]/self::div[@class="openblock"]', output, 1 end # NOTE this is not consistent w/ AsciiDoc output, but this is some screwy input anyway =begin test "consecutive list continuation lines are folded" do input = <<-EOS Lists ===== * Item one, paragraph one + + Item one, paragraph two + + * Item two + + EOS output = render_string input assert_xpath '//ul', output, 1 assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '//ul/li[1]//p', output, 2 assert_xpath '//ul/li[1]//p[text() = "Item one, paragraph one"]', output, 1 assert_xpath '//ul/li[1]//p[text() = "Item one, paragraph two"]', output, 1 end =end end end context "Ordered lists (:olist)" do context "Simple lists" do test "dot elements with no blank lines" do input = <<-EOS List ==== . Foo . Boo . Blech EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 3 end test 'indented dot elements using spaces' do input = <<-EOS . Foo . Boo . Blech EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 3 end test 'indented dot elements using tabs' do input = <<-EOS \t.\tFoo \t.\tBoo \t.\tBlech EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 3 end test 'should represent explicit role attribute as style class' do input = <<-EOS [role="dry"] . Once . Again . Refactor! EOS output = render_embedded_string input assert_css '.olist.arabic.dry', output, 1 assert_css '.olist ol.arabic', output, 1 end test 'should represent custom numbering and explicit role attribute as style classes' do input = <<-EOS [loweralpha, role="dry"] . Once . Again . Refactor! EOS output = render_embedded_string input assert_css '.olist.loweralpha.dry', output, 1 assert_css '.olist ol.loweralpha', output, 1 end test 'should represent implicit role attribute as style class' do input = <<-EOS [.dry] . Once . Again . Refactor! EOS output = render_embedded_string input assert_css '.olist.arabic.dry', output, 1 assert_css '.olist ol.arabic', output, 1 end test 'should represent custom numbering and implicit role attribute as style classes' do input = <<-EOS [loweralpha.dry] . Once . Again . Refactor! EOS output = render_embedded_string input assert_css '.olist.loweralpha.dry', output, 1 assert_css '.olist ol.loweralpha', output, 1 end test "dot elements separated by blank lines should merge lists" do input = <<-EOS List ==== . Foo . Boo . Blech EOS output = render_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 3 end test 'dot elements with interspersed line comments should be skipped and not break list' do input = <<-EOS == List . Foo // line comment // another line comment . Boo // line comment more text // another line comment . Blech EOS output = render_embedded_string input assert_xpath '//ol', output, 1 assert_xpath '//ol/li', output, 3 assert_xpath %((//ol/li)[2]/p[text()="Boo\nmore text"]), output, 1 end test "dot elements separated by line comment offset by blank lines should not merge lists" do input = <<-EOS List ==== . Foo . Boo // . Blech EOS output = render_string input assert_xpath '//ol', output, 2 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '(//ol)[2]/li', output, 1 end test "dot elements separated by a block title offset by a blank line should not merge lists" do input = <<-EOS List ==== . Foo . Boo .Also . Blech EOS output = render_string input assert_xpath '//ol', output, 2 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '(//ol)[2]/li', output, 1 assert_xpath '(//ol)[2]/preceding-sibling::*[@class = "title"][text() = "Also"]', output, 1 end test "dot elements separated by an attribute entry offset by a blank line should not merge lists" do input = <<-EOS == List . Foo . Boo :foo: bar . Blech EOS output = render_embedded_string input assert_xpath '//ol', output, 2 assert_xpath '(//ol)[1]/li', output, 2 assert_xpath '(//ol)[2]/li', output, 1 end end end context "Labeled lists (:dlist)" do context "Simple lists" do test "single-line adjacent elements" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line indented adjacent elements" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line indented adjacent elements with tabs" do input = <<-EOS term1::\tdef1 \tterm2::\tdef2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line elements separated by blank line should create a single list" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 end test "a line comment between elements should divide them into separate lists" do input = <<-EOS term1:: def1 // term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl/dt', output, 2 assert_xpath '(//dl)[1]/dt', output, 1 assert_xpath '(//dl)[2]/dt', output, 1 end test "a ruler between elements should divide them into separate lists" do input = <<-EOS term1:: def1 ''' term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl//hr', output, 0 assert_xpath '(//dl)[1]/dt', output, 1 assert_xpath '(//dl)[2]/dt', output, 1 end test "a block title between elements should divide them into separate lists" do input = <<-EOS term1:: def1 .Some more term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl/dt', output, 2 assert_xpath '(//dl)[1]/dt', output, 1 assert_xpath '(//dl)[2]/dt', output, 1 assert_xpath '(//dl)[2]/preceding-sibling::*[@class="title"][text() = "Some more"]', output, 1 end test "multi-line elements with paragraph content" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line elements with indented paragraph content" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test 'multi-line element with paragraph starting with multiple dashes should not be seen as list' do input = <<-EOS term1:: def1 -- and a note term2:: def2 EOS output = render_embedded_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath %((//dl/dt)[1]/following-sibling::dd/p[text() = "def1#{entity 8201}#{entity 8212}#{entity 8201}and a note"]), output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line element with multiple terms" do input = <<-EOS term1:: term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dd', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dt', output, 1 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test 'consecutive terms share same varlistentry in docbook' do input = <<-EOS term:: alt term:: definition last:: EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '//varlistentry', output, 2 assert_xpath '(//varlistentry)[1]/term', output, 2 assert_xpath '(//varlistentry)[2]/term', output, 1 assert_xpath '(//varlistentry)[2]/listitem', output, 1 assert_xpath '(//varlistentry)[2]/listitem[normalize-space(text())=""]', output, 1 end test "multi-line elements with blank line before paragraph content" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line elements with paragraph and literal content" do # blank line following literal paragraph is required or else it will gobble up the second term input = <<-EOS term1:: def1 literal term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '//dl/dt/following-sibling::dd//pre', output, 1 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "mixed single and multi-line adjacent elements" do input = <<-EOS term1:: def1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dt/following-sibling::dd', output, 2 assert_xpath '(//dl/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "element with anchor" do input = <<-EOS [[term1]]term1:: def1 [[term2]]term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dt', output, 2 assert_xpath '(//dl/dt)[1]/a[@id = "term1"]', output, 1 assert_xpath '(//dl/dt)[2]/a[@id = "term2"]', output, 1 end test "missing space before term does not produce labeled list" do input = <<-EOS term1::def1 term2::def2 EOS output = render_string input assert_xpath '//dl', output, 0 end test "literal block inside labeled list" do input = <<-EOS term:: + .... literal, line 1 literal, line 2 .... anotherterm:: def EOS output = render_string input assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dd', output, 2 assert_xpath '//dl/dd//pre', output, 1 assert_xpath '(//dl/dd)[1]/*[@class="literalblock"]//pre', output, 1 assert_xpath '(//dl/dd)[2]/p[text() = "def"]', output, 1 end test "literal block inside labeled list with trailing line continuation" do input = <<-EOS term:: + .... literal, line 1 literal, line 2 .... + anotherterm:: def EOS output = render_string input assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dd', output, 2 assert_xpath '//dl/dd//pre', output, 1 assert_xpath '(//dl/dd)[1]/*[@class="literalblock"]//pre', output, 1 assert_xpath '(//dl/dd)[2]/p[text() = "def"]', output, 1 end test "multiple listing blocks inside labeled list" do input = <<-EOS term:: + ---- listing, line 1 listing, line 2 ---- + ---- listing, line 1 listing, line 2 ---- anotherterm:: def EOS output = render_string input assert_xpath '//dl/dt', output, 2 assert_xpath '//dl/dd', output, 2 assert_xpath '//dl/dd//pre', output, 2 assert_xpath '(//dl/dd)[1]/*[@class="listingblock"]//pre', output, 2 assert_xpath '(//dl/dd)[2]/p[text() = "def"]', output, 1 end test "open block inside labeled list" do input = <<-EOS term:: + -- Open block as definition of term. And some more detail... -- anotherterm:: def EOS output = render_string input assert_xpath '//dl/dd//p', output, 3 assert_xpath '(//dl/dd)[1]//*[@class="openblock"]//p', output, 2 end test "paragraph attached by a list continuation on either side in a labeled list" do input = <<-EOS term1:: def1 + more detail + term2:: def2 EOS output = render_string input assert_xpath '(//dl/dt)[1][normalize-space(text())="term1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text())="term2"]', output, 1 assert_xpath '(//dl/dd)[1]//p', output, 2 assert_xpath '((//dl/dd)[1]//p)[1][text()="def1"]', output, 1 assert_xpath '(//dl/dd)[1]/p/following-sibling::*[@class="paragraph"]/p[text() = "more detail"]', output, 1 end test "paragraph attached by a list continuation on either side to a multi-line element in a labeled list" do input = <<-EOS term1:: def1 + more detail + term2:: def2 EOS output = render_string input assert_xpath '(//dl/dt)[1][normalize-space(text())="term1"]', output, 1 assert_xpath '(//dl/dt)[2][normalize-space(text())="term2"]', output, 1 assert_xpath '(//dl/dd)[1]//p', output, 2 assert_xpath '((//dl/dd)[1]//p)[1][text()="def1"]', output, 1 assert_xpath '(//dl/dd)[1]/p/following-sibling::*[@class="paragraph"]/p[text() = "more detail"]', output, 1 end test "verse paragraph inside a labeled list" do input = <<-EOS term1:: def + [verse] la la la term2:: def EOS output = render_string input assert_xpath '//dl/dd//p', output, 2 assert_xpath '(//dl/dd)[1]/*[@class="verseblock"]/pre[text() = "la la la"]', output, 1 end test "list inside a labeled list" do input = <<-EOS term1:: * level 1 ** level 2 * level 1 term2:: def EOS output = render_string input assert_xpath '//dl/dd', output, 2 assert_xpath '//dl/dd/p', output, 1 assert_xpath '(//dl/dd)[1]//ul', output, 2 assert_xpath '((//dl/dd)[1]//ul)[1]//ul', output, 1 end test "list inside a labeled list offset by blank lines" do input = <<-EOS term1:: * level 1 ** level 2 * level 1 term2:: def EOS output = render_string input assert_xpath '//dl/dd', output, 2 assert_xpath '//dl/dd/p', output, 1 assert_xpath '(//dl/dd)[1]//ul', output, 2 assert_xpath '((//dl/dd)[1]//ul)[1]//ul', output, 1 end test "should only grab one line following last item if item has no inline definition" do input = <<-EOS term1:: def1 term2:: def2 A new paragraph Another new paragraph EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dd', output, 2 assert_xpath '(//dl/dd)[1]/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dd)[2]/p[text() = "def2"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 2 assert_xpath '(//*[@class="dlist"]/following-sibling::*[@class="paragraph"])[1]/p[text() = "A new paragraph"]', output, 1 assert_xpath '(//*[@class="dlist"]/following-sibling::*[@class="paragraph"])[2]/p[text() = "Another new paragraph"]', output, 1 end test "should only grab one literal line following last item if item has no inline definition" do input = <<-EOS term1:: def1 term2:: def2 A new paragraph Another new paragraph EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dd', output, 2 assert_xpath '(//dl/dd)[1]/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dd)[2]/p[text() = "def2"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 2 assert_xpath '(//*[@class="dlist"]/following-sibling::*[@class="paragraph"])[1]/p[text() = "A new paragraph"]', output, 1 assert_xpath '(//*[@class="dlist"]/following-sibling::*[@class="paragraph"])[2]/p[text() = "Another new paragraph"]', output, 1 end test "should append subsequent paragraph literals to list item as block content" do input = <<-EOS term1:: def1 term2:: def2 literal A new paragraph. EOS output = render_string input assert_xpath '//dl', output, 1 assert_xpath '//dl/dd', output, 2 assert_xpath '(//dl/dd)[1]/p[text() = "def1"]', output, 1 assert_xpath '(//dl/dd)[2]/p[text() = "def2"]', output, 1 assert_xpath '(//dl/dd)[2]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '(//dl/dd)[2]/p/following-sibling::*[@class="literalblock"]//pre[text() = "literal"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '(//*[@class="dlist"]/following-sibling::*[@class="paragraph"])[1]/p[text() = "A new paragraph."]', output, 1 end test 'should not match comment line that looks like labeled list term' do input = <<-EOS * item //:: == Section section text EOS output = render_embedded_string input assert_xpath '/*[@class="ulist"]', output, 1 assert_xpath '/*[@class="sect1"]', output, 1 assert_xpath '/*[@class="sect1"]/h2[text()="Section"]', output, 1 assert_xpath '/*[@class="ulist"]/following-sibling::*[@class="sect1"]', output, 1 end end context "Nested lists" do test "single-line adjacent nested elements" do input = <<-EOS term1:: def1 label1::: detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line adjacent maximum nested elements" do input = <<-EOS term1:: def1 label1::: detail1 name1:::: value1 item1;; price1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 4 assert_xpath '//dl//dl//dl//dl', output, 1 end test "single-line nested elements seperated by blank line at top level" do input = <<-EOS term1:: def1 label1::: detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line nested elements seperated by blank line at nested level" do input = <<-EOS term1:: def1 label1::: detail1 label2::: detail2 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "single-line adjacent nested elements with alternate delimiters" do input = <<-EOS term1:: def1 label1;; detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line adjacent nested elements" do input = <<-EOS term1:: def1 label1::: detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line nested elements seperated by blank line at nested level repeated" do input = <<-EOS term1:: def1 label1::: detail1 label2::: detail2 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '(//dl//dl/dt)[1][normalize-space(text()) = "label1"]', output, 1 assert_xpath '(//dl//dl/dt)[1]/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl//dl/dt)[2][normalize-space(text()) = "label2"]', output, 1 assert_xpath '(//dl//dl/dt)[2]/following-sibling::dd/p[text() = "detail2"]', output, 1 end test "multi-line element with indented nested element" do input = <<-EOS term1:: def1 label1;; detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt', output, 2 assert_xpath '(//dl)[1]/dd', output, 2 assert_xpath '((//dl)[1]/dt)[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '((//dl)[1]/dt)[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '((//dl)[1]/dt)[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '((//dl)[1]/dt)[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "mixed single and multi-line elements with indented nested elements" do input = <<-EOS term1:: def1 label1::: detail1 term2:: def2 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[text() = "def1"]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 assert_xpath '(//dl)[1]/dt[2][normalize-space(text()) = "term2"]', output, 1 assert_xpath '(//dl)[1]/dt[2]/following-sibling::dd/p[text() = "def2"]', output, 1 end test "multi-line elements with first paragraph folded to text with adjacent nested element" do input = <<-EOS term1:: def1 continued label1::: detail1 EOS output = render_string input assert_xpath '//dl', output, 2 assert_xpath '//dl//dl', output, 1 assert_xpath '(//dl)[1]/dt[1][normalize-space(text()) = "term1"]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[starts-with(text(), "def1")]', output, 1 assert_xpath '(//dl)[1]/dt[1]/following-sibling::dd/p[contains(text(), "continued")]', output, 1 assert_xpath '//dl//dl/dt[normalize-space(text()) = "label1"]', output, 1 assert_xpath '//dl//dl/dt/following-sibling::dd/p[text() = "detail1"]', output, 1 end end context 'Special lists' do test 'should render glossary list with proper semantics' do input = <<-EOS [glossary] term 1:: def 1 term 2:: def 2 EOS output = render_embedded_string input assert_css '.dlist.glossary', output, 1 assert_css '.dlist dt:not([class])', output, 2 end test 'consecutive glossary terms should share same glossentry element in docbook' do input = <<-EOS [glossary] term:: alt term:: definition last:: EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/glossentry', output, 2 assert_xpath '(/glossentry)[1]/glossterm', output, 2 assert_xpath '(/glossentry)[2]/glossterm', output, 1 assert_xpath '(/glossentry)[2]/glossdef', output, 1 assert_xpath '(/glossentry)[2]/glossdef[normalize-space(text())=""]', output, 1 end test 'should render horizontal list with proper markup' do input = <<-EOS [horizontal] first term:: definition + more detail second term:: definition EOS output = render_embedded_string input assert_css '.hdlist', output, 1 assert_css '.hdlist table', output, 1 assert_css '.hdlist table colgroup', output, 0 assert_css '.hdlist table tr', output, 2 assert_xpath '/*[@class="hdlist"]/table/tr[1]/td', output, 2 assert_xpath '/*[@class="hdlist"]/table/tr[1]/td[@class="hdlist1"]', output, 1 assert_xpath '/*[@class="hdlist"]/table/tr[1]/td[@class="hdlist2"]', output, 1 assert_xpath '/*[@class="hdlist"]/table/tr[1]/td[@class="hdlist2"]/p', output, 1 assert_xpath '/*[@class="hdlist"]/table/tr[1]/td[@class="hdlist2"]/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '((//tr)[1]/td)[1][normalize-space(text())="first term"]', output, 1 assert_xpath '((//tr)[1]/td)[2]/p[normalize-space(text())="definition"]', output, 1 assert_xpath '/*[@class="hdlist"]/table/tr[2]/td', output, 2 assert_xpath '((//tr)[2]/td)[1][normalize-space(text())="second term"]', output, 1 assert_xpath '((//tr)[2]/td)[2]/p[normalize-space(text())="definition"]', output, 1 end test 'should set col widths of item and label if specified' do input = <<-EOS [horizontal] [labelwidth="25", itemwidth="75"] term:: def EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup', output, 1 assert_css 'table > colgroup > col', output, 2 assert_xpath '(//table/colgroup/col)[1][@style="width:25%;"]', output, 1 assert_xpath '(//table/colgroup/col)[2][@style="width:75%;"]', output, 1 end test 'should add strong class to label if strong option is set' do input = <<-EOS [horizontal, options="strong"] term:: def EOS output = render_embedded_string input assert_css '.hdlist', output, 1 assert_css '.hdlist td.hdlist1.strong', output, 1 end test 'consecutive terms in horizontal list should share same cell' do input = <<-EOS [horizontal] term:: alt term:: definition last:: EOS output = render_embedded_string input assert_xpath '//tr', output, 2 assert_xpath '(//tr)[1]/td[@class="hdlist1"]', output, 1 # NOTE I'm trimming the trailing
    in Asciidoctor #assert_xpath '(//tr)[1]/td[@class="hdlist1"]/br', output, 2 assert_xpath '(//tr)[1]/td[@class="hdlist1"]/br', output, 1 assert_xpath '(//tr)[2]/td[@class="hdlist2"]', output, 1 end test 'consecutive terms in horizontal list should share same entry in docbook' do input = <<-EOS [horizontal] term:: alt term:: definition last:: EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '//row', output, 2 assert_xpath '(//row)[1]/entry', output, 2 assert_xpath '((//row)[1]/entry)[1]/simpara', output, 2 assert_xpath '(//row)[2]/entry', output, 2 assert_xpath '((//row)[2]/entry)[2][normalize-space(text())=""]', output, 1 end test 'should render horizontal list in docbook with proper markup' do input = <<-EOS .Terms [horizontal] first term:: definition + more detail second term:: definition EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/table', output, 1 assert_xpath '/table[@tabstyle="horizontal"]', output, 1 assert_xpath '/table[@tabstyle="horizontal"]/title[text()="Terms"]', output, 1 assert_xpath '/table//row', output, 2 assert_xpath '(/table//row)[1]/entry', output, 2 assert_xpath '(/table//row)[2]/entry', output, 2 assert_xpath '((/table//row)[1]/entry)[2]/simpara', output, 2 end test 'should render qanda list in HTML with proper semantics' do input = <<-EOS [qanda] Question 1:: Answer 1. Question 2:: Answer 2. EOS output = render_embedded_string input assert_css '.qlist.qanda', output, 1 assert_css '.qanda > ol', output, 1 assert_css '.qanda > ol > li', output, 2 (1..2).each do |idx| assert_css ".qanda > ol > li:nth-child(#{idx}) > p", output, 2 assert_css ".qanda > ol > li:nth-child(#{idx}) > p:first-child > em", output, 1 assert_xpath "/*[@class = 'qlist qanda']/ol/li[#{idx}]/p[1]/em[normalize-space(text()) = 'Question #{idx}']", output, 1 assert_css ".qanda > ol > li:nth-child(#{idx}) > p:last-child > *", output, 0 assert_xpath "/*[@class = 'qlist qanda']/ol/li[#{idx}]/p[2][normalize-space(text()) = 'Answer #{idx}.']", output, 1 end end test 'should render qanda list in DocBook with proper semantics' do input = <<-EOS [qanda] Question 1:: Answer 1. Question 2:: Answer 2. EOS output = render_embedded_string input, :backend => 'docbook' assert_css 'qandaset', output, 1 assert_css 'qandaset > qandaentry', output, 2 (1..2).each do |idx| assert_css "qandaset > qandaentry:nth-child(#{idx}) > question", output, 1 assert_css "qandaset > qandaentry:nth-child(#{idx}) > question > simpara", output, 1 assert_xpath "/qandaset/qandaentry[#{idx}]/question/simpara[normalize-space(text()) = 'Question #{idx}']", output, 1 assert_css "qandaset > qandaentry:nth-child(#{idx}) > answer", output, 1 assert_css "qandaset > qandaentry:nth-child(#{idx}) > answer > simpara", output, 1 assert_xpath "/qandaset/qandaentry[#{idx}]/answer/simpara[normalize-space(text()) = 'Answer #{idx}.']", output, 1 end end test 'consecutive questions should share same question element in docbook' do input = <<-EOS [qanda] question:: follow-up question:: response last question:: EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '//qandaentry', output, 2 assert_xpath '(//qandaentry)[1]/question', output, 1 assert_xpath '(//qandaentry)[1]/question/simpara', output, 2 assert_xpath '(//qandaentry)[2]/question', output, 1 assert_xpath '(//qandaentry)[2]/answer', output, 1 assert_xpath '(//qandaentry)[2]/answer[normalize-space(text())=""]', output, 1 end test 'should render bibliography list with proper semantics' do input = <<-EOS [bibliography] - [[[taoup]]] Eric Steven Raymond. 'The Art of Unix Programming'. Addison-Wesley. ISBN 0-13-142901-9. - [[[walsh-muellner]]] Norman Walsh & Leonard Muellner. 'DocBook - The Definitive Guide'. O'Reilly & Associates. 1999. ISBN 1-56592-580-7. EOS output = render_embedded_string input assert_css '.ulist.bibliography', output, 1 assert_css '.ulist.bibliography ul', output, 1 assert_css '.ulist.bibliography ul li', output, 2 assert_css '.ulist.bibliography ul li p', output, 2 assert_css '.ulist.bibliography ul li:nth-child(1) p a#taoup', output, 1 assert_xpath '//a/*', output, 0 text = xmlnodes_at_xpath '(//a)[1]/following-sibling::text()', output, 1 assert text.text.start_with?('[taoup] ') end end end context 'Labeled lists redux' do context 'Label without text on same line' do test 'folds text from subsequent line' do input = <<-EOS == Lists term1:: def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text from first line after blank lines' do input = <<-EOS == Lists term1:: def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text from first line after blank line and immediately preceding next item' do input = <<-EOS == Lists term1:: def1 term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 2 assert_xpath '(//*[@class="dlist"]//dd)[1]/p[text()="def1"]', output, 1 end test 'paragraph offset by blank lines does not break list if label does not have inline text' do input = <<-EOS == Lists term1:: def1 term2:: def2 EOS output = render_embedded_string input assert_css 'dl', output, 1 assert_css 'dl > dt', output, 2 assert_css 'dl > dd', output, 2 assert_xpath '(//dl/dd)[1]/p[text()="def1"]', output, 1 end test 'folds text from first line after comment line' do input = <<-EOS == Lists term1:: // comment def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text from line following comment line offset by blank line' do input = <<-EOS == Lists term1:: // comment def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text from subsequent indented line' do input = <<-EOS == Lists term1:: def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text from indented line after blank line' do input = <<-EOS == Lists term1:: def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 end test 'folds text that looks like ruler offset by blank line' do input = <<-EOS == Lists term1:: ''' EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p/em[text()="'"]), output, 1 end test 'folds text that looks like ruler offset by blank line and line comment' do input = <<-EOS == Lists term1:: // comment ''' EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p/em[text()="'"]), output, 1 end test 'folds text that looks like ruler and the line following it offset by blank line' do input = <<-EOS == Lists term1:: ''' continued EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p/em[text()="'"]), output, 1 assert_xpath %(//*[@class="dlist"]//dd/p[normalize-space(text())="continued"]), output, 1 end test 'folds text that looks like title offset by blank line' do input = <<-EOS == Lists term1:: .def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()=".def1"]', output, 1 end test 'folds text that looks like title offset by blank line and line comment' do input = <<-EOS == Lists term1:: // comment .def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()=".def1"]', output, 1 end test 'folds text that looks like admonition offset by blank line' do input = <<-EOS == Lists term1:: NOTE: def1 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="NOTE: def1"]', output, 1 end test 'folds text of first literal line offset by blank line appends subsequent literals offset by blank line as blocks' do input = <<-EOS == Lists term1:: def1 literal literal EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]', output, 2 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 2 end test 'folds text of subsequent line and appends following literal line offset by blank line as block if term has no inline definition' do input = <<-EOS == Lists term1:: def1 literal term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 2 assert_xpath '(//*[@class="dlist"]//dd)[1]/p[text()="def1"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 end test 'appends literal line attached by continuation as block if item has no inline definition' do input = <<-EOS == Lists term1:: + literal EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]//pre[text()="literal"]', output, 1 end test 'appends literal line attached by continuation as block if item has no inline definition followed by ruler' do input = <<-EOS == Lists term1:: + literal ''' EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]//pre[text()="literal"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::hr', output, 1 end test 'appends line attached by continuation as block if item has no inline definition followed by ruler' do input = <<-EOS == Lists term1:: + para ''' EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::hr', output, 1 end test 'appends line attached by continuation as block if item has no inline definition followed by block' do input = <<-EOS == Lists term1:: + para .... literal .... EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 end test 'appends block attached by continuation but not subsequent block not attached by continuation' do input = <<-EOS == Lists term1:: + .... literal .... .... detached .... EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]//pre[text()="literal"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="literalblock"]//pre[text()="detached"]', output, 1 end test 'appends list if item has no inline definition' do input = <<-EOS == Lists term1:: * one * two * three EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd//ul/li', output, 3 end test 'appends list to first term when followed immediately by second term' do input = <<-EOS == Lists term1:: * one * two * three term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 2 assert_xpath '(//*[@class="dlist"]//dd)[1]/p', output, 0 assert_xpath '(//*[@class="dlist"]//dd)[1]//ul/li', output, 3 assert_xpath '(//*[@class="dlist"]//dd)[2]/p[text()="def2"]', output, 1 end test 'appends indented list to first term that is adjacent to second term' do input = <<-EOS == Lists label 1:: definition 1 * one * two * three label 2:: definition 2 paragraph EOS output = render_embedded_string input assert_css '.dlist > dl', output, 1 assert_css '.dlist dt', output, 2 assert_xpath '(//*[@class="dlist"]//dt)[1][normalize-space(text())="label 1"]', output, 1 assert_xpath '(//*[@class="dlist"]//dt)[2][normalize-space(text())="label 2"]', output, 1 assert_css '.dlist dd', output, 2 assert_xpath '(//*[@class="dlist"]//dd)[1]/p[text()="definition 1"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[2]/p[text()="definition 2"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="ulist"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="ulist"]//li', output, 3 assert_css '.dlist + .paragraph', output, 1 end test 'appends indented list to first term that is attached by a continuation and adjacent to second term' do input = <<-EOS == Lists label 1:: definition 1 + * one * two * three label 2:: definition 2 paragraph EOS output = render_embedded_string input assert_css '.dlist > dl', output, 1 assert_css '.dlist dt', output, 2 assert_xpath '(//*[@class="dlist"]//dt)[1][normalize-space(text())="label 1"]', output, 1 assert_xpath '(//*[@class="dlist"]//dt)[2][normalize-space(text())="label 2"]', output, 1 assert_css '.dlist dd', output, 2 assert_xpath '(//*[@class="dlist"]//dd)[1]/p[text()="definition 1"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[2]/p[text()="definition 2"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="ulist"]', output, 1 assert_xpath '(//*[@class="dlist"]//dd)[1]/p/following-sibling::*[@class="ulist"]//li', output, 3 assert_css '.dlist + .paragraph', output, 1 end test 'appends list and paragraph block when line following list attached by continuation' do input = <<-EOS == Lists term1:: * one * two * three + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/ul/li', output, 3 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'first continued line associated with nested list item and second continued line associated with term' do input = <<-EOS == Lists term1:: * one + nested list para + term1 para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/ul/li', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/ul/li/*[@class="paragraph"]/p[text()="nested list para"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="ulist"]/following-sibling::*[@class="paragraph"]/p[text()="term1 para"]', output, 1 end test 'literal line attached by continuation swallows adjacent line that looks like term' do input = <<-EOS == Lists term1:: + literal notnestedterm::: + literal notnestedterm::: EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]', output, 2 assert_xpath %(//*[@class="dlist"]//dd/*[@class="literalblock"]//pre[text()=" literal\nnotnestedterm:::"]), output, 2 end test 'line attached by continuation is appended as paragraph if term has no inline definition' do input = <<-EOS == Lists term1:: + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'attached paragraph does not break on adjacent nested labeled list term' do input = <<-EOS term1:: def + more definition not a term::: def EOS output = render_embedded_string input assert_css '.dlist > dl > dt', output, 1 assert_css '.dlist > dl > dd', output, 1 assert_css '.dlist > dl > dd > .paragraph', output, 1 assert output.include?('not a term::: def') end # FIXME pending =begin test 'attached paragraph does not break on adjacent sibling labeled list term' do input = <<-EOS term1:: def + more definition not a term:: def EOS output = render_embedded_string input assert_css '.dlist > dl > dt', output, 1 assert_css '.dlist > dl > dd', output, 1 assert_css '.dlist > dl > dd > .paragraph', output, 1 assert output.include?('not a term:: def') end =end test 'attached styled paragraph does not break on adjacent nested labeled list term' do input = <<-EOS term1:: def + [quote] more definition not a term::: def EOS output = render_embedded_string input assert_css '.dlist > dl > dt', output, 1 assert_css '.dlist > dl > dd', output, 1 assert_css '.dlist > dl > dd > .quoteblock', output, 1 assert output.include?('not a term::: def') end test 'appends line as paragraph if attached by continuation following blank line and line comment when term has no inline definition' do input = <<-EOS == Lists term1:: // comment + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'line attached by continuation offset by blank line is appended as paragraph if term has no inline definition' do input = <<-EOS == Lists term1:: + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p', output, 0 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'delimited block breaks list even when term has no inline definition' do input = <<-EOS == Lists term1:: ==== detached ==== EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 0 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="exampleblock"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="exampleblock"]//p[text()="detached"]', output, 1 end test 'attribute line breaks list even when term has no inline definition' do input = <<-EOS == Lists term1:: [verse] detached EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 0 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="verseblock"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="verseblock"]/pre[text()="detached"]', output, 1 end test 'id line breaks list even when term has no inline definition' do input = <<-EOS == Lists term1:: [[id]] detached EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 0 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]/p[text()="detached"]', output, 1 end end context 'Item with text inline' do test 'folds text from inline definition and subsequent line' do input = <<-EOS == Lists term1:: def1 continued EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p[text()="def1\ncontinued"]), output, 1 end test 'folds text from inline definition and subsequent lines' do input = <<-EOS == Lists term1:: def1 continued continued EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p[text()="def1\ncontinued\ncontinued"]), output, 1 end test 'folds text from inline definition and line following comment line' do input = <<-EOS == Lists term1:: def1 // comment continued EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p[text()="def1\ncontinued"]), output, 1 end test 'folds text from inline definition and subsequent indented line' do input = <<-EOS == Lists term1:: def1 continued EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath %(//*[@class="dlist"]//dd/p[text()="def1\ncontinued"]), output, 1 end test 'appends literal line offset by blank line as block if item has inline definition' do input = <<-EOS == Lists term1:: def1 literal EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 end test 'appends literal line offset by blank line as block and appends line after continuation as block if item has inline definition' do input = <<-EOS == Lists term1:: def1 literal + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="literalblock"]/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'appends line after continuation as block and literal line offset by blank line as block if item has inline definition' do input = <<-EOS == Lists term1:: def1 + para literal EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/*[@class="paragraph"]/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 end test 'appends list if item has inline definition' do input = <<-EOS == Lists term1:: def1 * one * two * three EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="ulist"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="ulist"]/ul/li', output, 3 end test 'appends literal line attached by continuation as block if item has inline definition followed by ruler' do input = <<-EOS == Lists term1:: def1 + literal ''' EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="literalblock"]//pre[text()="literal"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::hr', output, 1 end test 'line offset by blank line breaks list if term has inline definition' do input = <<-EOS == Lists term1:: def1 detached EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]/p[text()="detached"]', output, 1 end test 'nested term with definition does not consume following heading' do input = <<-EOS == Lists term:: def nestedterm;; nesteddef Detached ~~~~~~~~ EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 2 assert_xpath '//*[@class="dlist"]//dd', output, 2 assert_xpath '//*[@class="dlist"]/dl//dl', output, 1 assert_xpath '//*[@class="dlist"]/dl//dl/dt', output, 1 assert_xpath '((//*[@class="dlist"])[1]//dd)[1]/p[text()="def"]', output, 1 assert_xpath '((//*[@class="dlist"])[1]//dd)[1]/p/following-sibling::*[@class="dlist"]', output, 1 assert_xpath '((//*[@class="dlist"])[1]//dd)[1]/p/following-sibling::*[@class="dlist"]//dd/p[text()="nesteddef"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="sect2"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="sect2"]/h3[text()="Detached"]', output, 1 end test 'line attached by continuation is appended as paragraph if term has inline definition followed by detached paragraph' do input = <<-EOS == Lists term1:: def1 + para detached EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="paragraph"]/p[text()="detached"]', output, 1 end test 'line attached by continuation is appended as paragraph if term has inline definition followed by detached block' do input = <<-EOS == Lists term1:: def1 + para **** detached **** EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="sidebarblock"]', output, 1 assert_xpath '//*[@class="dlist"]/following-sibling::*[@class="sidebarblock"]//p[text()="detached"]', output, 1 end test 'line attached by continuation offset by line comment is appended as paragraph if term has inline definition' do input = <<-EOS == Lists term1:: def1 // comment + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'line attached by continuation offset by blank line is appended as paragraph if term has inline definition' do input = <<-EOS == Lists term1:: def1 + para EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 1 assert_xpath '//*[@class="dlist"]//dd', output, 1 assert_xpath '//*[@class="dlist"]//dd/p[text()="def1"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '//*[@class="dlist"]//dd/p/following-sibling::*[@class="paragraph"]/p[text()="para"]', output, 1 end test 'line comment offset by blank line divides lists because item has text' do input = <<-EOS == Lists term1:: def1 // term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 2 end test 'ruler offset by blank line divides lists because item has text' do input = <<-EOS == Lists term1:: def1 ''' term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 2 end test 'block title offset by blank line divides lists and becomes title of second list because item has text' do input = <<-EOS == Lists term1:: def1 .title term2:: def2 EOS output = render_embedded_string input assert_xpath '//*[@class="dlist"]/dl', output, 2 assert_xpath '(//*[@class="dlist"])[2]/*[@class="title"][text()="title"]', output, 1 end end end context 'Callout lists' do test 'listing block with sequential callouts followed by adjacent callout list' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <3> ---- <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 1 assert_xpath '//programlisting//co', output, 3 assert_xpath '(//programlisting//co)[1][@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting//co)[2][@id = "CO1-2"]', output, 1 assert_xpath '(//programlisting//co)[3][@id = "CO1-3"]', output, 1 assert_xpath '//programlisting/following-sibling::calloutlist/callout', output, 3 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[2][@arearefs = "CO1-2"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[3][@arearefs = "CO1-3"]', output, 1 end test 'listing block with sequential callouts followed by non-adjacent callout list' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <3> ---- Paragraph. <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 1 assert_xpath '//programlisting//co', output, 3 assert_xpath '(//programlisting//co)[1][@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting//co)[2][@id = "CO1-2"]', output, 1 assert_xpath '(//programlisting//co)[3][@id = "CO1-3"]', output, 1 assert_xpath '//programlisting/following-sibling::*[1][self::simpara]', output, 1 assert_xpath '//programlisting/following-sibling::calloutlist/callout', output, 3 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[2][@arearefs = "CO1-2"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[3][@arearefs = "CO1-3"]', output, 1 end test 'listing block with a callout that refers to two different lines' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <2> ---- <1> Import the library <2> Where the magic happens EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 1 assert_xpath '//programlisting//co', output, 3 assert_xpath '(//programlisting//co)[1][@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting//co)[2][@id = "CO1-2"]', output, 1 assert_xpath '(//programlisting//co)[3][@id = "CO1-3"]', output, 1 assert_xpath '//programlisting/following-sibling::calloutlist/callout', output, 2 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[2][@arearefs = "CO1-2 CO1-3"]', output, 1 end test 'listing block with non-sequential callouts followed by adjacent callout list' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <2> doc = Asciidoctor::Document.new('Hello, World!') # <3> puts doc.render # <1> ---- <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 1 assert_xpath '//programlisting//co', output, 3 assert_xpath '(//programlisting//co)[1][@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting//co)[2][@id = "CO1-2"]', output, 1 assert_xpath '(//programlisting//co)[3][@id = "CO1-3"]', output, 1 assert_xpath '//programlisting/following-sibling::calloutlist/callout', output, 3 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[1][@arearefs = "CO1-3"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[2][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//programlisting/following-sibling::calloutlist/callout)[3][@arearefs = "CO1-2"]', output, 1 end test 'two listing blocks can share the same callout list' do input = <<-EOS .Import library [source, ruby] ---- require 'asciidoctor' # <1> ---- .Use library [source, ruby] ---- doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <3> ---- <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 2 assert_xpath '(//programlisting)[1]//co', output, 1 assert_xpath '(//programlisting)[1]//co[@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting)[2]//co', output, 2 assert_xpath '((//programlisting)[2]//co)[1][@id = "CO1-2"]', output, 1 assert_xpath '((//programlisting)[2]//co)[2][@id = "CO1-3"]', output, 1 assert_xpath '(//calloutlist/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//calloutlist/callout)[2][@arearefs = "CO1-2"]', output, 1 assert_xpath '(//calloutlist/callout)[3][@arearefs = "CO1-3"]', output, 1 end test 'two listing blocks each followed by an adjacent callout list' do input = <<-EOS .Import library [source, ruby] ---- require 'asciidoctor' # <1> ---- <1> Describe the first line .Use library [source, ruby] ---- doc = Asciidoctor::Document.new('Hello, World!') # <1> puts doc.render # <2> ---- <1> Describe the second line <2> Describe the third line EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//programlisting', output, 2 assert_xpath '(//programlisting)[1]//co', output, 1 assert_xpath '(//programlisting)[1]//co[@id = "CO1-1"]', output, 1 assert_xpath '(//programlisting)[2]//co', output, 2 assert_xpath '((//programlisting)[2]//co)[1][@id = "CO2-1"]', output, 1 assert_xpath '((//programlisting)[2]//co)[2][@id = "CO2-2"]', output, 1 assert_xpath '//calloutlist', output, 2 assert_xpath '(//calloutlist)[1]/callout', output, 1 assert_xpath '((//calloutlist)[1]/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//calloutlist)[2]/callout', output, 2 assert_xpath '((//calloutlist)[2]/callout)[1][@arearefs = "CO2-1"]', output, 1 assert_xpath '((//calloutlist)[2]/callout)[2][@arearefs = "CO2-2"]', output, 1 end test 'callout list with block content' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <3> ---- <1> Imports the library as a RubyGem <2> Creates a new document * Scans the lines for known blocks * Converts the lines into blocks <3> Renders the document + You can write this to file rather than printing to stdout. EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//calloutlist', output, 1 assert_xpath '//calloutlist/callout', output, 3 assert_xpath '(//calloutlist/callout)[1]/*', output, 1 assert_xpath '(//calloutlist/callout)[2]/para', output, 1 assert_xpath '(//calloutlist/callout)[2]/itemizedlist', output, 1 assert_xpath '(//calloutlist/callout)[3]/para', output, 1 assert_xpath '(//calloutlist/callout)[3]/simpara', output, 1 end test 'escaped callout should not be interpreted as a callout' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # \\<1> ---- EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//co', output, 0 end test 'should not recognize callouts in middle of line' do input = <<-EOS [source, ruby] ---- puts "The syntax <1> at the end of the line makes a code callout" ---- EOS output = render_embedded_string input assert_xpath '//b', output, 0 end test 'should allow multiple callouts on the same line' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' <1> doc = Asciidoctor.load('Hello, World!') # <2> <3> <4> puts doc.render <5><6> exit 0 ---- <1> Require library <2> Load document from String <3> Uses default backend and doctype <4> One more for good luck <5> Renders document to String <6> Prints output to stdout EOS output = render_embedded_string input assert_xpath '//code/b', output, 6 assert_match(/ \(1\)<\/b>$/, output) assert_match(/ \(2\)<\/b> \(3\)<\/b> \(4\)<\/b>$/, output) assert_match(/ \(5\)<\/b>\(6\)<\/b>$/, output) end test 'should allow XML comment-style callouts' do input = <<-EOS [source, xml] ----
    Section Title Just a paragraph
    ---- <1> The title is required <2> The content isn't EOS output = render_embedded_string input assert_xpath '//b', output, 2 assert_xpath '//b[text()="(1)"]', output, 1 assert_xpath '//b[text()="(2)"]', output, 1 end test 'should not allow callouts with half an XML comment' do input = <<-EOS ---- First line <1--> Second line <2--> ---- EOS output = render_embedded_string input assert_xpath '//b', output, 0 end test 'should not recognize callouts in an indented labeled list paragraph' do input = <<-EOS foo:: bar <1> <1> Not pointing to a callout EOS output = render_embedded_string input assert_xpath '//dl//b', output, 0 assert_xpath '//dl/dd/p[text()="bar <1>"]', output, 1 assert_xpath '//ol/li/p[text()="Not pointing to a callout"]', output, 1 end test 'should not recognize callouts in an indented outline list paragraph' do input = <<-EOS * foo bar <1> <1> Not pointing to a callout EOS output = render_embedded_string input assert_xpath '//ul//b', output, 0 assert_xpath %(//ul/li/p[text()="foo\nbar <1>"]), output, 1 assert_xpath '//ol/li/p[text()="Not pointing to a callout"]', output, 1 end test 'should remove leading line comment chars' do input = <<-EOS ---- puts 'Hello, world!' # <1> ---- <1> Ruby ---- println 'Hello, world!' // <1> ---- <1> Groovy ---- (def hello (fn [] "Hello, world!")) ;; <1> (hello) ---- <1> Clojure EOS output = render_embedded_string input assert_xpath '//b', output, 3 nodes = xmlnodes_at_css 'pre', output assert_equal "puts 'Hello, world!' (1)", nodes[0].text assert_equal "println 'Hello, world!' (1)", nodes[1].text assert_equal %((def hello (fn [] "Hello, world!")) (1)\n(hello)), nodes[2].text end test 'literal block with callouts' do input = <<-EOS .... Roses are red <1> Violets are blue <2> .... <1> And so is Ruby <2> But violet is more like purple EOS output = render_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '//literallayout', output, 1 assert_xpath '//literallayout//co', output, 2 assert_xpath '(//literallayout//co)[1][@id = "CO1-1"]', output, 1 assert_xpath '(//literallayout//co)[2][@id = "CO1-2"]', output, 1 assert_xpath '//literallayout/following-sibling::*[1][self::calloutlist]/callout', output, 2 assert_xpath '(//literallayout/following-sibling::*[1][self::calloutlist]/callout)[1][@arearefs = "CO1-1"]', output, 1 assert_xpath '(//literallayout/following-sibling::*[1][self::calloutlist]/callout)[2][@arearefs = "CO1-2"]', output, 1 end test 'callout list with icons enabled' do input = <<-EOS [source, ruby] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') # <2> puts doc.render # <3> ---- <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_embedded_string input, :attributes => {'icons' => ''} assert_css '.listingblock code > img', output, 3 (1..3).each do |i| assert_xpath %((/div[@class="listingblock"]//code/img)[#{i}][@src="./images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1 end assert_css '.colist table td img', output, 3 (1..3).each do |i| assert_xpath %((/div[@class="colist arabic"]//td/img)[#{i}][@src="./images/icons/callouts/#{i}.png"][@alt="#{i}"]), output, 1 end end test 'callout list with font-based icons enabled' do input = <<-EOS [source] ---- require 'asciidoctor' # <1> doc = Asciidoctor::Document.new('Hello, World!') #<2> puts doc.render #<3> ---- <1> Describe the first line <2> Describe the second line <3> Describe the third line EOS output = render_embedded_string input, :attributes => {'icons' => 'font'} assert_css '.listingblock code > i', output, 3 (1..3).each do |i| assert_xpath %((/div[@class="listingblock"]//code/i)[#{i}]), output, 1 assert_xpath %((/div[@class="listingblock"]//code/i)[#{i}][@class="conum"][@data-value="#{i}"]), output, 1 assert_xpath %((/div[@class="listingblock"]//code/i)[#{i}]/following-sibling::b[text()="(#{i})"]), output, 1 end assert_css '.colist table td i', output, 3 (1..3).each do |i| assert_xpath %((/div[@class="colist arabic"]//td/i)[#{i}]), output, 1 assert_xpath %((/div[@class="colist arabic"]//td/i)[#{i}][@class="conum"][@data-value = "#{i}"]), output, 1 assert_xpath %((/div[@class="colist arabic"]//td/i)[#{i}]/following-sibling::b[text() = "#{i}"]), output, 1 end end end context 'Checklists' do test 'should create checklist if at least one item has checkbox syntax' do input = <<-EOS - [ ] todo - [x] done - plain EOS output = render_embedded_string input assert_css '.ulist.checklist', output, 1 assert_css '.ulist.checklist li input[type="checkbox"][disabled]', output, 2 assert_css '.ulist.checklist li input[type="checkbox"][checked]', output, 1 assert_xpath '(/*[@class="ulist checklist"]/ul/li)[3]/p[text()="plain"]', output, 1 end test 'should create checklist with font icons if at least one item has checkbox syntax and icons attribute is font' do input = <<-EOS - [ ] todo - [x] done - plain EOS output = render_embedded_string input, :attributes => {'icons' => 'font'} assert_css '.ulist.checklist', output, 1 assert_css '.ulist.checklist li i.icon-check', output, 1 assert_css '.ulist.checklist li i.icon-check-empty', output, 1 assert_xpath '(/*[@class="ulist checklist"]/ul/li)[3]/p[text()="plain"]', output, 1 end test 'should create interactive checklist if interactive option is set even with icons attribute is font' do input = <<-EOS :icons: font [options="interactive"] - [ ] todo - [x] done EOS output = render_embedded_string input assert_css '.ulist.checklist', output, 1 assert_css '.ulist.checklist li input[type="checkbox"]', output, 2 assert_css '.ulist.checklist li input[type="checkbox"][disabled]', output, 0 assert_css '.ulist.checklist li input[type="checkbox"][checked]', output, 1 end end context 'Lists model' do test 'content should return items in list' do input = <<-EOS * one * two * three EOS doc = document_from_string input list = doc.blocks.first assert list.is_a? Asciidoctor::List items = list.items assert_equal 3, items.size assert_equal list.items, list.content end end asciidoctor-0.1.4/test/options_test.rb000066400000000000000000000102471221220517700200600ustar00rootroot00000000000000require 'test_helper' require 'asciidoctor/cli/options' context 'Options' do test 'should return error code 0 when help flag is present' do redirect_streams do |stdout, stderr| exitval = Asciidoctor::Cli::Options.parse!(%w(-h)) assert_equal 0, exitval assert_match(/^Usage:/, stdout.string) end end test 'should return error code 1 when invalid option present' do redirect_streams do |stdout, stderr| exitval = Asciidoctor::Cli::Options.parse!(%w(--foobar)) assert_equal 1, exitval assert_equal 'asciidoctor: invalid option: --foobar', stderr.string.chomp end end test 'should return error code 1 when option has invalid argument' do redirect_streams do |stdout, stderr| exitval = Asciidoctor::Cli::Options.parse!(%w(-d chapter input.ad)) # had to change for #320 assert_equal 1, exitval assert_equal 'asciidoctor: invalid argument: -d chapter', stderr.string.chomp end end test 'should return error code 1 when option is missing required argument' do redirect_streams do |stdout, stderr| exitval = Asciidoctor::Cli::Options.parse!(%w(-b)) assert_equal 1, exitval assert_equal 'asciidoctor: option missing argument: -b', stderr.string.chomp end end test 'should emit warning when unparsed options remain' do redirect_streams do |stdout, stderr| options = Asciidoctor::Cli::Options.parse!(%w(-b docbook - -)) assert options.is_a? Hash assert_match(/asciidoctor: WARNING: extra arguments .*/, stderr.string.chomp) end end test 'basic argument assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-v -s -d book test/fixtures/sample.asciidoc)) assert_equal true, options[:verbose] assert_equal false, options[:header_footer] assert_equal 'book', options[:attributes]['doctype'] assert_equal 1, options[:input_files].size assert_equal 'test/fixtures/sample.asciidoc', options[:input_files][0] end test 'standard attribute assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-a imagesdir=images,icons test/fixtures/sample.asciidoc)) assert_equal 'images', options[:attributes]['imagesdir'] assert_equal '', options[:attributes]['icons'] end test 'multiple attribute arguments' do options = Asciidoctor::Cli::Options.parse!(%w(-a imagesdir=images -a icons test/fixtures/sample.asciidoc)) assert_equal 'images', options[:attributes]['imagesdir'] assert_equal '', options[:attributes]['icons'] end test 'should only split attribute key/value pairs on first equal sign' do options = Asciidoctor::Cli::Options.parse!(%w(-a name=value=value test/fixtures/sample.asciidoc)) assert_equal 'value=value', options[:attributes]['name'] end test 'should allow any backend to be specified' do options = Asciidoctor::Cli::Options.parse!(%w(-b my_custom_backend test/fixtures/sample.asciidoc)) assert_equal 'my_custom_backend', options[:attributes]['backend'] end test 'article doctype assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-d article test/fixtures/sample.asciidoc)) assert_equal 'article', options[:attributes]['doctype'] end test 'book doctype assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-d book test/fixtures/sample.asciidoc)) assert_equal 'book', options[:attributes]['doctype'] end test 'inline doctype assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-d inline test/fixtures/sample.asciidoc)) assert_equal 'inline', options[:attributes]['doctype'] end test 'template engine assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-E haml test/fixtures/sample.asciidoc)) assert_equal 'haml', options[:template_engine] end test 'template directory assignment' do options = Asciidoctor::Cli::Options.parse!(%w(-T custom-backend test/fixtures/sample.asciidoc)) assert_equal ['custom-backend'], options[:template_dirs] end test 'multiple template directory assignments' do options = Asciidoctor::Cli::Options.parse!(%w(-T custom-backend -T custom-backend-hacks test/fixtures/sample.asciidoc)) assert_equal ['custom-backend', 'custom-backend-hacks'], options[:template_dirs] end end asciidoctor-0.1.4/test/paragraphs_test.rb000066400000000000000000000331411221220517700205130ustar00rootroot00000000000000require 'test_helper' context 'Paragraphs' do context 'Normal' do test 'should treat plain text separated by blank lines as paragraphs' do input = <<-EOS Plain text for the win! Yep. Text. Plain and simple. EOS output = render_embedded_string input assert_css 'p', output, 2 assert_xpath '(//p)[1][text() = "Plain text for the win!"]', output, 1 assert_xpath '(//p)[2][text() = "Yep. Text. Plain and simple."]', output, 1 end test 'should associate block title with paragraph' do input = <<-EOS .Titled Paragraph. Winning. EOS output = render_embedded_string input assert_css 'p', output, 2 assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"]', output, 1 assert_xpath '(//p)[1]/preceding-sibling::*[@class = "title"][text() = "Titled"]', output, 1 assert_xpath '(//p)[2]/preceding-sibling::*[@class = "title"]', output, 0 end test 'no duplicate block before next section' do input = <<-EOS = Title Preamble == First Section Paragraph 1 Paragraph 2 == Second Section Last words EOS output = render_string input assert_xpath '//p[text() = "Paragraph 2"]', output, 1 end test 'does not treat wrapped line as a list item' do input = <<-EOS paragraph . wrapped line EOS output = render_embedded_string input assert_css 'p', output, 1 assert_xpath %(//p[text()="paragraph\n. wrapped line"]), output, 1 end test 'does not treat wrapped line as a block title' do input = <<-EOS paragraph .wrapped line EOS output = render_embedded_string input assert_css 'p', output, 1 assert_xpath %(//p[text()="paragraph\n.wrapped line"]), output, 1 end test 'interprets normal paragraph style as normal paragraph' do input = <<-EOS [normal] Normal paragraph. Nothing special. EOS output = render_embedded_string input assert_css 'p', output, 1 end test 'removes indentation from literal paragraph marked as normal' do input = <<-EOS [normal] Normal paragraph. Nothing special. Last line. EOS output = render_embedded_string input assert_css 'p', output, 1 assert_xpath %(//p[text()="Normal paragraph.\n Nothing special.\nLast line."]), output, 1 end test 'normal paragraph terminates at block attribute list' do input = <<-EOS normal text [literal] literal text EOS output = render_embedded_string input assert_css '.paragraph:root', output, 1 assert_css '.literalblock:root', output, 1 end test 'normal paragraph terminates at block delimiter' do input = <<-EOS normal text -- text in open block -- EOS output = render_embedded_string input assert_css '.paragraph:root', output, 1 assert_css '.openblock:root', output, 1 end test 'normal paragraph terminates at list continuation' do input = <<-EOS normal text + EOS output = render_embedded_string input assert_css '.paragraph:root', output, 2 assert_xpath %((/*[@class="paragraph"])[1]/p[text() = "normal text"]), output, 1 assert_xpath %((/*[@class="paragraph"])[2]/p[text() = "+"]), output, 1 end test 'normal style turns literal paragraph into normal paragraph' do input = <<-EOS [normal] normal paragraph, despite the leading indent EOS output = render_embedded_string input assert_css '.paragraph:root > p', output, 1 end test 'expands index term macros in DocBook backend' do input = <<-EOS Here is an index entry for ((tigers)). indexterm:[Big cats,Tigers,Siberian Tiger] Here is an index entry for indexterm2:[Linux]. (((Operating Systems,Linux,Fedora))) Note that multi-entry terms generate separate index entries. EOS output = render_embedded_string input, :attributes => {'backend' => 'docbook45'} assert_xpath '/simpara', output, 1 term1 = (xmlnodes_at_xpath '(//indexterm)[1]', output, 1).first assert_equal 'tigers', term1.to_s assert term1.next.content.start_with?('tigers') term2 = (xmlnodes_at_xpath '(//indexterm)[2]', output, 1).first term2_elements = term2.elements assert_equal 3, term2_elements.size assert_equal 'Big cats', term2_elements[0].to_s assert_equal 'Tigers', term2_elements[1].to_s assert_equal 'Siberian Tiger', term2_elements[2].to_s term3 = (xmlnodes_at_xpath '(//indexterm)[3]', output, 1).first term3_elements = term3.elements assert_equal 2, term3_elements.size assert_equal 'Tigers', term3_elements[0].to_s assert_equal 'Siberian Tiger', term3_elements[1].to_s term4 = (xmlnodes_at_xpath '(//indexterm)[4]', output, 1).first term4_elements = term4.elements assert_equal 1, term4_elements.size assert_equal 'Siberian Tiger', term4_elements[0].to_s term5 = (xmlnodes_at_xpath '(//indexterm)[5]', output, 1).first assert_equal 'Linux', term5.to_s assert term5.next.content.start_with?('Linux') assert_xpath '(//indexterm)[6]/*', output, 3 assert_xpath '(//indexterm)[7]/*', output, 2 assert_xpath '(//indexterm)[8]/*', output, 1 end test 'normal paragraph should honor explicit subs list' do input = <<-EOS [subs="specialcharacters"] *Hey Jude* EOS output = render_embedded_string input assert output.include?('*Hey Jude*') end end context 'Literal' do test 'single-line literal paragraphs' do input = <<-EOS LITERALS ARE LITERALLY AWESOME! EOS output = render_embedded_string input assert_xpath '//pre', output, 3 end test 'multi-line literal paragraph' do input = <<-EOS Install instructions: yum install ruby rubygems gem install asciidoctor You're good to go! EOS output = render_embedded_string input assert_xpath '//pre', output, 1 # indentation should be trimmed from literal block assert_xpath %(//pre[text() = "yum install ruby rubygems\ngem install asciidoctor"]), output, 1 end test 'literal paragraph' do input = <<-EOS [literal] this text is literally literal EOS output = render_embedded_string input assert_xpath %(/*[@class="literalblock"]//pre[text()="this text is literally literal"]), output, 1 end test 'should read content below literal style verbatim' do input = <<-EOS [literal] image::not-an-image-block[] EOS output = render_embedded_string input assert_xpath %(/*[@class="literalblock"]//pre[text()="image::not-an-image-block[]"]), output, 1 assert_css 'img', output, 0 end test 'listing paragraph' do input = <<-EOS [listing] this text is a listing EOS output = render_embedded_string input assert_xpath %(/*[@class="listingblock"]//pre[text()="this text is a listing"]), output, 1 end test 'source paragraph' do input = <<-EOS [source] use the source, luke! EOS output = render_embedded_string input assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[text()="use the source, luke!"]), output, 1 end test 'source code paragraph with language' do input = <<-EOS [source, perl] die 'zomg perl sucks'; EOS output = render_embedded_string input assert_xpath %(/*[@class="listingblock"]//pre[@class="highlight"]/code[@class="perl language-perl"][text()="die 'zomg perl sucks';"]), output, 1 end test 'literal paragraph terminates at block attribute list' do input = <<-EOS literal text [normal] normal text EOS output = render_embedded_string input assert_xpath %(/*[@class="literalblock"]), output, 1 assert_xpath %(/*[@class="paragraph"]), output, 1 end test 'literal paragraph terminates at block delimiter' do input = <<-EOS literal text -- normal text -- EOS output = render_embedded_string input assert_xpath %(/*[@class="literalblock"]), output, 1 assert_xpath %(/*[@class="openblock"]), output, 1 end test 'literal paragraph terminates at list continuation' do input = <<-EOS literal text + EOS output = render_embedded_string input assert_xpath %(/*[@class="literalblock"]), output, 1 assert_xpath %(/*[@class="literalblock"]//pre[text() = "literal text"]), output, 1 assert_xpath %(/*[@class="paragraph"]), output, 1 assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1 end end context 'Quote' do test "single-line quote paragraph" do input = <<-EOS [quote] Famous quote. EOS output = render_string input assert_xpath '//*[@class = "quoteblock"]', output, 1 assert_xpath '//*[@class = "quoteblock"]//p', output, 0 assert_xpath '//*[@class = "quoteblock"]//*[contains(text(), "Famous quote.")]', output, 1 end test 'quote paragraph terminates at list continuation' do input = <<-EOS [quote] A famouse quote. + EOS output = render_embedded_string input assert_css '.quoteblock:root', output, 1 assert_css '.paragraph:root', output, 1 assert_xpath %(/*[@class="paragraph"]/p[text() = "+"]), output, 1 end test "verse paragraph" do output = render_string("[verse]\nFamous verse.") assert_xpath '//*[@class = "verseblock"]', output, 1 assert_xpath '//*[@class = "verseblock"]/pre', output, 1 assert_xpath '//*[@class = "verseblock"]//p', output, 0 assert_xpath '//*[@class = "verseblock"]/pre[normalize-space(text()) = "Famous verse."]', output, 1 end test 'quote paragraph should honor explicit subs list' do input = <<-EOS [subs="specialcharacters"] [quote] *Hey Jude* EOS output = render_embedded_string input assert output.include?('*Hey Jude*') end end context "special" do test "note multiline syntax" do Asciidoctor::ADMONITION_STYLES.each do |style| assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("[#{style}]\nThis is a winner.") end end test "note block syntax" do Asciidoctor::ADMONITION_STYLES.each do |style| assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("[#{style}]\n====\nThis is a winner.\n====") end end test "note inline syntax" do Asciidoctor::ADMONITION_STYLES.each do |style| assert_xpath "//div[@class='admonitionblock #{style.downcase}']", render_string("#{style}: This is important, fool!") end end test "sidebar block" do input = <<-EOS == Section .Sidebar **** Content goes here **** EOS result = render_string(input) assert_xpath "//*[@class='sidebarblock']//p", result, 1 end context 'Styled Paragraphs' do test 'should wrap text in simpara for styled paragraphs when rendered to DocBook' do input = <<-EOS = Book :doctype: book [preface] = About this book [abstract] An abstract for the book. = Part 1 [partintro] An intro to this part. [sidebar] Just a side note. [example] As you can see here. [quote] Wise words from a wise person. EOS output = render_string input, :backend => 'docbook' assert_css 'abstract > simpara', output, 1 assert_css 'partintro > simpara', output, 1 assert_css 'sidebar > simpara', output, 1 assert_css 'informalexample > simpara', output, 1 assert_css 'blockquote > simpara', output, 1 end test 'should wrap text in simpara for styled paragraphs with title when rendered to DocBook' do input = <<-EOS = Book :doctype: book [preface] = About this book [abstract] .Abstract title An abstract for the book. = Part 1 [partintro] .Part intro title An intro to this part. [sidebar] .Sidebar title Just a side note. [example] .Example title As you can see here. [quote] .Quote title Wise words from a wise person. EOS output = render_string input, :backend => 'docbook' assert_css 'abstract > title', output, 1 assert_xpath '//abstract/title[text() = "Abstract title"]', output, 1 assert_css 'abstract > title + simpara', output, 1 assert_css 'partintro > title', output, 1 assert_xpath '//partintro/title[text() = "Part intro title"]', output, 1 assert_css 'partintro > title + simpara', output, 1 assert_css 'sidebar > title', output, 1 assert_xpath '//sidebar/title[text() = "Sidebar title"]', output, 1 assert_css 'sidebar > title + simpara', output, 1 assert_css 'example > title', output, 1 assert_xpath '//example/title[text() = "Example title"]', output, 1 assert_css 'example > title + simpara', output, 1 assert_css 'blockquote > title', output, 1 assert_xpath '//blockquote/title[text() = "Quote title"]', output, 1 assert_css 'blockquote > title + simpara', output, 1 end end context 'Inline doctype' do test 'should only format and output text in first paragraph when doctype is inline' do input = "http://asciidoc.org[AsciiDoc] is a _lightweight_ markup language...\n\nignored" output = render_string input, :doctype => 'inline' assert_equal 'AsciiDoc is a lightweight markup language…', output end test 'should output empty string if first block is not a paragraph' do input = '* bullet' output = render_string input, :doctype => 'inline' assert output.empty? end end end end asciidoctor-0.1.4/test/paths_test.rb000066400000000000000000000214401221220517700175010ustar00rootroot00000000000000require 'test_helper' context 'Path Resolver' do context 'Web Paths' do def setup @resolver = Asciidoctor::PathResolver.new end test 'target with absolute path' do assert_equal '/images', @resolver.web_path('/images') assert_equal '/images', @resolver.web_path('/images', '') assert_equal '/images', @resolver.web_path('/images', nil) end test 'target with relative path' do assert_equal 'images', @resolver.web_path('images') assert_equal 'images', @resolver.web_path('images', '') assert_equal 'images', @resolver.web_path('images', nil) end test 'target with path relative to current directory' do assert_equal './images', @resolver.web_path('./images') assert_equal './images', @resolver.web_path('./images', '') assert_equal './images', @resolver.web_path('./images', nil) end test 'target with absolute path ignores start path' do assert_equal '/images', @resolver.web_path('/images', 'foo') assert_equal '/images', @resolver.web_path('/images', '/foo') assert_equal '/images', @resolver.web_path('/images', './foo') end test 'target with relative path appended to start path' do assert_equal 'assets/images', @resolver.web_path('images', 'assets') assert_equal '/assets/images', @resolver.web_path('images', '/assets') assert_equal './assets/images', @resolver.web_path('images', './assets') end test 'target with path relative to current directory appended to start path' do assert_equal 'assets/images', @resolver.web_path('./images', 'assets') assert_equal '/assets/images', @resolver.web_path('./images', '/assets') assert_equal './assets/images', @resolver.web_path('./images', './assets') end test 'target with relative path appended to url start path' do assert_equal 'http://www.example.com/assets/images', @resolver.web_path('images', 'http://www.example.com/assets') end test 'normalize target' do assert_equal '../images', @resolver.web_path('../images/../images') end test 'append target to start path and normalize' do assert_equal '../images', @resolver.web_path('../images/../images', '../images') assert_equal '../../images', @resolver.web_path('../images', '..') end test 'normalize parent directory that follows root' do assert_equal '/tiger.png', @resolver.web_path('/../tiger.png') assert_equal '/tiger.png', @resolver.web_path('/../../tiger.png') end test 'uses start when target is empty' do assert_equal 'assets/images', @resolver.web_path('', 'assets/images') assert_equal 'assets/images', @resolver.web_path(nil, 'assets/images') end test 'posixfies windows paths' do assert_equal '/images', @resolver.web_path('\\images') assert_equal '../images', @resolver.web_path('..\\images') assert_equal '/images', @resolver.web_path('\\..\\images') assert_equal 'assets/images', @resolver.web_path('assets\\images') assert_equal '../assets/images', @resolver.web_path('assets\\images', '..\\images\\..') end end context 'System Paths' do JAIL = '/home/doctor/docs' def setup @resolver = Asciidoctor::PathResolver.new end test 'prevents access to paths outside of jail' do assert_equal "#{JAIL}/css", @resolver.system_path('../../../../../css', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/css", @resolver.system_path('/../../../../../css', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/css", @resolver.system_path('../../../css', '../../..', JAIL) end test 'throws exception for illegal path access if recover is false' do begin @resolver.system_path('../../../../../css', "#{JAIL}/assets/stylesheets", JAIL, :recover => false) flunk 'Expecting SecurityError to be raised' rescue SecurityError end end test 'resolves start path if target is empty' do assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path(nil, "#{JAIL}/assets/stylesheets", JAIL) end test 'resolves start path if target is dot' do assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('.', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('./', "#{JAIL}/assets/stylesheets", JAIL) end test 'treats absolute target as relative when jail is specified' do assert_equal "#{JAIL}/assets/stylesheets", @resolver.system_path('/', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/assets/stylesheets/foo", @resolver.system_path('/foo', "#{JAIL}/assets/stylesheets", JAIL) assert_equal "#{JAIL}/assets/foo", @resolver.system_path('/../foo', "#{JAIL}/assets/stylesheets", JAIL) end test 'allows use of absolute target or start if resolved path is sub-path of jail' do assert_equal "#{JAIL}/my/path", @resolver.system_path("#{JAIL}/my/path", '', JAIL) assert_equal "#{JAIL}/my/path", @resolver.system_path("#{JAIL}/my/path", nil, JAIL) assert_equal "#{JAIL}/my/path", @resolver.system_path('', "#{JAIL}/my/path", JAIL) assert_equal "#{JAIL}/my/path", @resolver.system_path(nil, "#{JAIL}/my/path", JAIL) assert_equal "#{JAIL}/my/path", @resolver.system_path('path', "#{JAIL}/my", JAIL) end test 'uses jail path if start path is empty' do assert_equal "#{JAIL}/images/tiger.png", @resolver.system_path('images/tiger.png', '', JAIL) assert_equal "#{JAIL}/images/tiger.png", @resolver.system_path('images/tiger.png', nil, JAIL) end test 'raises security error if start is not contained within jail' do begin @resolver.system_path('images/tiger.png', '/etc', JAIL) flunk 'Expecting SecurityError to be raised' rescue SecurityError end begin @resolver.system_path('.', '/etc', JAIL) flunk 'Expecting SecurityError to be raised' rescue SecurityError end end test 'resolves absolute directory if jail is not specified' do assert_equal '/usr/share/stylesheet.css', @resolver.system_path('/usr/share/stylesheet.css', '/home/dallen/docs/assets/stylesheets') end test 'resolves ancestor directory of start if jail is not specified' do assert_equal '/usr/share/stylesheet.css', @resolver.system_path('../../../../../usr/share/stylesheet.css', '/home/dallen/docs/assets/stylesheets') end test 'resolves absolute path if start is absolute and target is relative' do assert_equal '/usr/share/assets/stylesheet.css', @resolver.system_path('assets/stylesheet.css', '/usr/share') end test 'resolves relative target relative to current directory if start is empty' do pwd = File.expand_path(Dir.pwd) assert_equal "#{pwd}/images/tiger.png", @resolver.system_path('images/tiger.png', '') assert_equal "#{pwd}/images/tiger.png", @resolver.system_path('images/tiger.png', nil) end test 'resolves and normalizes start with target is empty' do pwd = File.expand_path(Dir.pwd) assert_equal '/home/doctor/docs', @resolver.system_path('', '/home/doctor/docs') assert_equal '/home/doctor/docs', @resolver.system_path(nil, '/home/doctor/docs') assert_equal "#{pwd}/assets/images", @resolver.system_path(nil, 'assets/images') assert_equal "#{JAIL}/assets/images", @resolver.system_path('', '../assets/images', JAIL) end test 'posixfies windows paths' do assert_equal "#{JAIL}/assets/css", @resolver.system_path('..\\css', 'assets\\stylesheets', JAIL) end test 'resolves windows paths when file separator is backlash' do @resolver.file_separator = '\\' assert_equal 'C:/data/docs', @resolver.system_path('..', "C:\\data\\docs\\assets", 'C:\\data\\docs') assert_equal 'C:/data/docs', @resolver.system_path('..\\..', "C:\\data\\docs\\assets", 'C:\\data\\docs') assert_equal 'C:/data/docs/css', @resolver.system_path('..\\..\\css', "C:\\data\\docs\\assets", 'C:\\data\\docs') end test 'should calculate relative path' do filename = @resolver.system_path('part1/chapter1/section1.adoc', nil, JAIL) assert_equal "#{JAIL}/part1/chapter1/section1.adoc", filename assert_equal 'part1/chapter1/section1.adoc', @resolver.relative_path(filename, JAIL) end end context 'Helpers' do test 'rootname should return file name without extension' do assert_equal 'master', Asciidoctor::Helpers.rootname('master.adoc') assert_equal 'docs/master', Asciidoctor::Helpers.rootname('docs/master.adoc') end test 'rootname should file name if it has no extension' do assert_equal 'master', Asciidoctor::Helpers.rootname('master') assert_equal 'docs/master', Asciidoctor::Helpers.rootname('docs/master') end end end asciidoctor-0.1.4/test/preamble_test.rb000066400000000000000000000056031221220517700201540ustar00rootroot00000000000000require 'test_helper' context 'Preamble' do test 'title and single paragraph preamble before section' do input = <<-EOS Title ===== Preamble paragraph 1. == First Section Section paragraph 1. EOS result = render_string(input) assert_xpath '//p', result, 2 assert_xpath '//*[@id="preamble"]', result, 1 assert_xpath '//*[@id="preamble"]//p', result, 1 assert_xpath '//*[@id="preamble"]/following-sibling::*//h2[@id="_first_section"]', result, 1 assert_xpath '//*[@id="preamble"]/following-sibling::*//p', result, 1 end test 'title and multi-paragraph preamble before section' do input = <<-EOS Title ===== Preamble paragraph 1. Preamble paragraph 2. == First Section Section paragraph 1. EOS result = render_string(input) assert_xpath '//p', result, 3 assert_xpath '//*[@id="preamble"]', result, 1 assert_xpath '//*[@id="preamble"]//p', result, 2 assert_xpath '//*[@id="preamble"]/following-sibling::*//h2[@id="_first_section"]', result, 1 assert_xpath '//*[@id="preamble"]/following-sibling::*//p', result, 1 end test 'title and preamble only' do input = <<-EOS Title ===== Preamble paragraph 1. EOS result = render_string(input) assert_xpath '//p', result, 1 assert_xpath '//*[@id="preamble"]', result, 1 assert_xpath '//*[@id="preamble"]//p', result, 1 assert_xpath '//*[@id="preamble"]/following-sibling::*', result, 0 end test 'title and section without preamble' do input = <<-EOS Title ===== == First Section Section paragraph 1. EOS result = render_string(input) assert_xpath '//p', result, 1 assert_xpath '//*[@id="preamble"]', result, 0 assert_xpath '//h2[@id="_first_section"]', result, 1 end test 'no title with preamble and section' do input = <<-EOS Preamble paragraph 1. == First Section Section paragraph 1. EOS result = render_string(input) assert_xpath '//p', result, 2 assert_xpath '//*[@id="preamble"]', result, 0 assert_xpath '//h2[@id="_first_section"]/preceding::p', result, 1 end test 'preamble in book doctype' do input = <<-EOS Book ==== :doctype: book Back then... = Chapter One It was a dark and stormy night... = Chapter Two They couldn't believe their eyes when... EOS d = document_from_string(input) assert_equal 'book', d.doctype output = d.render assert_xpath '//h1', output, 3 assert_xpath %{//*[@id="preamble"]//p[text() = "Back then#{[8230].pack('U*')}"]}, output, 1 end test 'should render table of contents in preamble if toc-placement attribute value is preamble' do input = <<-EOS = Article :toc: :toc-placement: preamble Once upon a time... == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//*[@id="preamble"]/*[@id="toc"]', output, 1 end end asciidoctor-0.1.4/test/reader_test.rb000066400000000000000000001234321221220517700176300ustar00rootroot00000000000000require 'test_helper' class ReaderTest < Test::Unit::TestCase DIRNAME = File.expand_path(File.dirname(__FILE__)) SAMPLE_DATA = <<-EOS.each_line.to_a first line second line third line EOS context 'Reader' do context 'Prepare lines' do test 'should prepare lines from Array data' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA, reader.lines end test 'should prepare lines from String data' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA, reader.lines end end context 'With empty data' do test 'has_more_lines? should return false with empty data' do assert !Asciidoctor::Reader.new.has_more_lines? end test 'empty? should return true with empty data' do assert Asciidoctor::Reader.new.empty? assert Asciidoctor::Reader.new.eof? end test 'next_line_empty? should return true with empty data' do assert Asciidoctor::Reader.new.next_line_empty? end test 'peek_line should return nil with empty data' do assert_nil Asciidoctor::Reader.new.peek_line end test 'peek_lines should return empty Array with empty data' do assert_equal [], Asciidoctor::Reader.new.peek_lines end test 'read_line should return nil with empty data' do assert_nil Asciidoctor::Reader.new.read_line #assert_nil Asciidoctor::Reader.new.get_line end test 'read_lines should return empty Array with empty data' do assert_equal [], Asciidoctor::Reader.new.read_lines #assert_equal [], Asciidoctor::Reader.new.get_lines end end context 'With data' do test 'has_more_lines? should return true if there are lines remaining' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert reader.has_more_lines? end test 'empty? should return false if there are lines remaining' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert !reader.empty? assert !reader.eof? end test 'next_line_empty? should return false if next line is not blank' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert !reader.next_line_empty? end test 'next_line_empty? should return true if next line is blank' do reader = Asciidoctor::Reader.new ["\n", "second line\n"] assert reader.next_line_empty? end test 'peek_line should return next line if there are lines remaining' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA.first, reader.peek_line end test 'peek_line should not consume line or increment line number' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA.first, reader.peek_line assert_equal SAMPLE_DATA.first, reader.peek_line assert_equal 1, reader.lineno end test 'peek_line should return next lines if there are lines remaining' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) end test 'peek_lines should not consume lines or increment line number' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) assert_equal SAMPLE_DATA[0..1], reader.peek_lines(2) assert_equal 1, reader.lineno end test 'peek_lines should not invert order of lines' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA, reader.lines reader.peek_lines 3 assert_equal SAMPLE_DATA, reader.lines end test 'read_line should return next line if there are lines remaining' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA.first, reader.read_line end test 'read_line should consume next line and increment line number' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA[0], reader.read_line assert_equal SAMPLE_DATA[1], reader.read_line assert_equal 3, reader.lineno end test 'advance should consume next line and return a Boolean indicating if a line was consumed' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert reader.advance assert reader.advance assert reader.advance assert !reader.advance end test 'read_lines should return all lines' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA, reader.read_lines end test 'read should return all lines joined as String' do reader = Asciidoctor::Reader.new SAMPLE_DATA assert_equal SAMPLE_DATA.join, reader.read end test 'has_more_lines? should return false after read_lines is invoked' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.read_lines assert !reader.has_more_lines? end test 'unshift puts line onto Reader as next line to read' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.unshift "line zero\n" assert_equal "line zero\n", reader.peek_line assert_equal "line zero\n", reader.read_line assert_equal 1, reader.lineno end test 'terminate should consume all lines and update line number' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.terminate assert reader.eof? assert_equal 4, reader.lineno end test 'skip_blank_lines should skip blank lines' do reader = Asciidoctor::Reader.new ["", "\n"].concat(SAMPLE_DATA) reader.skip_blank_lines assert_equal SAMPLE_DATA.first, reader.peek_line end test 'lines should return remaining lines' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.read_line assert_equal SAMPLE_DATA[1..-1], reader.lines end test 'source_lines should return copy of original data Array' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.read_lines assert_equal SAMPLE_DATA, reader.source_lines end test 'source should return original data Array joined as String' do reader = Asciidoctor::Reader.new SAMPLE_DATA reader.read_lines assert_equal SAMPLE_DATA.join, reader.source end end context 'Line context' do test 'to_s should return file name and line number of current line' do reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' reader.read_line assert_equal 'sample.ad: line 2', reader.to_s end test 'line_info should return file name and line number of current line' do reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' reader.read_line assert_equal 'sample.ad: line 2', reader.line_info assert_equal 'sample.ad: line 2', reader.next_line_info end test 'prev_line_info should return file name and line number of previous line read' do reader = Asciidoctor::Reader.new SAMPLE_DATA, 'sample.ad' reader.read_line assert_equal 'sample.ad: line 1', reader.prev_line_info end end context 'Read lines until' do test 'Read lines until until end' do lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. EOS reader = Asciidoctor::Reader.new lines result = reader.read_lines_until assert_equal 3, result.size assert_equal lines, result assert !reader.has_more_lines? assert reader.eof? end test 'Read lines until until blank line' do lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. EOS reader = Asciidoctor::Reader.new lines result = reader.read_lines_until :break_on_blank_lines => true assert_equal 1, result.size assert_equal lines.first.chomp, result.first assert_equal lines.last, reader.peek_line end test 'Read lines until until blank line preserving last line' do lines = <<-EOS.each_line.to_a This is one paragraph. This is another paragraph. EOS reader = Asciidoctor::Reader.new lines result = reader.read_lines_until :break_on_blank_lines => true, :preserve_last_line => true assert_equal 1, result.size assert_equal lines.first.chomp, result.first assert reader.next_line_empty? end test 'Read lines until until condition is true' do lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. This is another paragraph inside the block. -- This is a paragraph outside the block. EOS reader = Asciidoctor::Reader.new lines reader.read_line result = reader.read_lines_until {|line| line.chomp == '--' } assert_equal 3, result.size assert_equal lines[1, 3], result assert reader.next_line_empty? end test 'Read lines until until condition is true, taking last line' do lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. This is another paragraph inside the block. -- This is a paragraph outside the block. EOS reader = Asciidoctor::Reader.new lines reader.read_line result = reader.read_lines_until(:read_last_line => true) {|line| line.chomp == '--' } assert_equal 4, result.size assert_equal lines[1, 4], result assert reader.next_line_empty? end test 'Read lines until until condition is true, taking and preserving last line' do lines = <<-EOS.each_line.to_a -- This is one paragraph inside the block. This is another paragraph inside the block. -- This is a paragraph outside the block. EOS reader = Asciidoctor::Reader.new lines reader.read_line result = reader.read_lines_until(:read_last_line => true, :preserve_last_line => true) {|line| line.chomp == '--' } assert_equal 4, result.size assert_equal lines[1, 4], result assert_equal "--\n", reader.peek_line end end end context 'PreprocessorReader' do context 'Type hierarchy' do test 'PreprocessorReader should extend from Reader' do doc = Asciidoctor::Document.new reader = Asciidoctor::PreprocessorReader.new doc assert reader.is_a?(Asciidoctor::Reader) end test 'PreprocessorReader should invoke or emulate Reader initializer' do doc = Asciidoctor::Document.new reader = Asciidoctor::PreprocessorReader.new doc, SAMPLE_DATA assert_equal SAMPLE_DATA, reader.lines assert_equal 1, reader.lineno end end context 'Prepare lines' do test 'should prepare and normalize lines from Array data' do doc = Asciidoctor::Document.new data = SAMPLE_DATA.map {|line| line.chomp} data.unshift '' data.push '' reader = Asciidoctor::PreprocessorReader.new doc, data assert_equal SAMPLE_DATA, reader.lines end test 'should prepare and normalize lines from String data' do doc = Asciidoctor::Document.new data = SAMPLE_DATA.map {|line| line.chomp} data.unshift ' ' data.push ' ' data_as_string = data * "\n" reader = Asciidoctor::PreprocessorReader.new doc, data_as_string assert_equal SAMPLE_DATA, reader.lines end test 'should clean CRLF from end of lines' do input = <<-EOS source\r with\r CRLF\r endlines\r EOS doc = Asciidoctor::Document.new [input, input.lines, input.split("\n"), input.split("\n").join].each do |lines| reader = Asciidoctor::PreprocessorReader.new doc, lines reader.lines.each do |line| assert !line.end_with?("\r"), "CRLF not properly cleaned for source lines: #{lines.inspect}" assert !line.end_with?("\r\n"), "CRLF not properly cleaned for source lines: #{lines.inspect}" assert line.end_with?("\n"), "CRLF not properly cleaned for source lines: #{lines.inspect}" end end end test 'should not skip front matter by default' do input = <<-EOS --- layout: post title: Document Title author: username tags: [ first, second ] --- = Document Title Author Name preamble EOS doc = Asciidoctor::Document.new reader = Asciidoctor::PreprocessorReader.new doc, input assert_equal '---', reader.peek_line.chomp end test 'should skip front matter if specified by skip-front-matter attribute' do front_matter = %(layout: post title: Document Title author: username tags: [ first, second ]) input = <<-EOS --- #{front_matter} --- = Document Title Author Name preamble EOS doc = Asciidoctor::Document.new [], :attributes => {'skip-front-matter' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input assert_equal '= Document Title', reader.peek_line.chomp assert_equal front_matter, doc.attributes['front-matter'] end end context 'Include Macro' do test 'include macro is disabled by default and becomes a link' do input = <<-EOS include::include-file.asciidoc[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input assert_equal 'link:include-file.asciidoc[]', reader.read_line.chomp end test 'include macro is enabled when safe mode is less than SECURE' do input = <<-EOS include::fixtures/include-file.asciidoc[] EOS doc = document_from_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME output = doc.render assert_match(/included content/, output) end test 'include macro should resolve file relative to current include' do input = <<-EOS include::fixtures/parent-include.adoc[] EOS pseudo_docfile = File.join DIRNAME, 'include-master.adoc' fixtures_dir = File.join DIRNAME, 'fixtures' parent_include_docfile = File.join fixtures_dir, 'parent-include.adoc' child_include_docfile = File.join fixtures_dir, 'child-include.adoc' grandchild_include_docfile = File.join fixtures_dir, 'grandchild-include.adoc' doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, input, pseudo_docfile assert_equal pseudo_docfile, reader.file assert_equal DIRNAME, reader.dir assert_equal 'include-master.adoc', reader.path assert_equal "first line of parent\n", reader.read_line assert_equal 'fixtures/parent-include.adoc: line 1', reader.prev_line_info assert_equal parent_include_docfile, reader.file assert_equal fixtures_dir, reader.dir assert_equal 'fixtures/parent-include.adoc', reader.path reader.skip_blank_lines assert_equal "first line of child\n", reader.read_line assert_equal 'fixtures/child-include.adoc: line 1', reader.prev_line_info assert_equal child_include_docfile, reader.file assert_equal fixtures_dir, reader.dir assert_equal 'fixtures/child-include.adoc', reader.path reader.skip_blank_lines assert_equal "first line of grandchild\n", reader.read_line assert_equal 'fixtures/grandchild-include.adoc: line 1', reader.prev_line_info assert_equal grandchild_include_docfile, reader.file assert_equal fixtures_dir, reader.dir assert_equal 'fixtures/grandchild-include.adoc', reader.path reader.skip_blank_lines assert_equal "last line of grandchild\n", reader.read_line reader.skip_blank_lines assert_equal "last line of child\n", reader.read_line reader.skip_blank_lines assert_equal "last line of parent\n", reader.read_line assert_equal 'fixtures/parent-include.adoc: line 5', reader.prev_line_info assert_equal parent_include_docfile, reader.file assert_equal fixtures_dir, reader.dir assert_equal 'fixtures/parent-include.adoc', reader.path end test 'missing file referenced by include macro does not crash processor' do input = <<-EOS include::fixtures/no-such-file.ad[] EOS begin doc = document_from_string input, :safe => :safe, :base_dir => DIRNAME assert_equal 0, doc.blocks.size rescue flunk 'include macro should not raise exception on missing file' end end test 'include macro can retrieve data from uri' do input = <<-EOS .... include::https://raw.github.com/asciidoctor/asciidoctor/master/LICENSE[] .... EOS output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} assert_match(/MIT/, output) end test 'inaccessible uri referenced by include macro does not crash processor' do input = <<-EOS .... include::http://127.0.0.1:0[] .... EOS begin output = render_embedded_string input, :safe => :safe, :attributes => {'allow-uri-read' => ''} assert_css 'pre:empty', output, 1 rescue flunk 'include macro should not raise exception on inaccessible uri' end end test 'include macro supports line selection' do input = <<-EOS include::fixtures/include-file.asciidoc[lines=1;3..4;6..-1] EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME assert_match(/first line/, output) assert_no_match(/second line/, output) assert_match(/third line/, output) assert_match(/fourth line/, output) assert_no_match(/fifth line/, output) assert_match(/sixth line/, output) assert_match(/seventh line/, output) assert_match(/eighth line/, output) assert_match(/last line of included content/, output) end test 'include macro supports line selection using quoted attribute value' do input = <<-EOS include::fixtures/include-file.asciidoc[lines="1, 3..4 , 6 .. -1"] EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME assert_match(/first line/, output) assert_no_match(/second line/, output) assert_match(/third line/, output) assert_match(/fourth line/, output) assert_no_match(/fifth line/, output) assert_match(/sixth line/, output) assert_match(/seventh line/, output) assert_match(/eighth line/, output) assert_match(/last line of included content/, output) end test 'include macro supports tagged selection' do input = <<-EOS include::fixtures/include-file.asciidoc[tag=snippetA] EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME assert_match(/snippetA content/, output) assert_no_match(/snippetB content/, output) assert_no_match(/non-tagged content/, output) assert_no_match(/included content/, output) end test 'include macro supports multiple tagged selection' do input = <<-EOS include::fixtures/include-file.asciidoc[tags=snippetA;snippetB] EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME assert_match(/snippetA content/, output) assert_match(/snippetB content/, output) assert_no_match(/non-tagged content/, output) assert_no_match(/included content/, output) end test 'lines attribute takes precedence over tags attribute in include macro' do input = <<-EOS include::fixtures/include-file.asciidoc[lines=1, tags=snippetA;snippetB] EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME assert_match(/first line of included content/, output) assert_no_match(/snippetA content/, output) assert_no_match(/snippetB content/, output) end test 'indent of included file can be reset to size of indent attribute' do input = <<-EOS [source, xml] ---- include::fixtures/basic-docinfo.xml[lines=2..3, indent=0] ---- EOS output = render_string input, :safe => :safe, :header_footer => false, :base_dir => DIRNAME result = xmlnodes_at_xpath('//pre', output, 1).text assert_equal "2013\nAcme, Inc.", result end test 'include processor is called to process include directive' do input = <<-EOS first line include::include-file.asciidoc[] last line EOS include_processor = Class.new { def initialize document end def handles? target true end def process reader, target, attributes content = ["include target:: #{target}\n", "\n", "middle line\n"] reader.push_include content, target, target, 1, attributes end } # Safe Mode is not required document = empty_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new document, input reader.instance_variable_set '@include_processors', [include_processor.new(document)] lines = [] lines << reader.read_line lines << reader.read_line lines << reader.read_line assert_equal "include target:: include-file.asciidoc\n", lines.last assert_equal 'include-file.asciidoc: line 2', reader.line_info while reader.has_more_lines? lines << reader.read_line end source = lines.join assert_match(/^include target:: include-file.asciidoc$/, source) assert_match(/^middle line$/, source) end test 'should fall back to built-in include macro behavior when not handled by include processor' do input = <<-EOS include::fixtures/include-file.asciidoc[] EOS include_processor = Class.new { def initialize document end def handles? target false end def process reader, target, attributes raise 'TestIncludeHandler should not have been invoked' end } document = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new document, input reader.instance_variable_set '@include_processors', [include_processor.new(document)] lines = reader.read_lines source = lines.join assert_match(/included content/, source) end test 'attributes are substituted in target of include macro' do input = <<-EOS :fixturesdir: fixtures :ext: asciidoc include::{fixturesdir}/include-file.{ext}[] EOS doc = document_from_string input, :safe => :safe, :base_dir => DIRNAME output = doc.render assert_match(/included content/, output) end test 'line is skipped by default if target of include macro resolves to empty' do input = <<-EOS include::{foodir}/include-file.asciidoc[] EOS doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, input assert_equal "include::{foodir}/include-file.asciidoc[]\n", reader.read_line end test 'line is dropped if target of include macro resolves to empty and attribute-missing attribute is not skip' do input = <<-EOS include::{foodir}/include-file.asciidoc[] EOS doc = empty_safe_document :base_dir => DIRNAME, :attributes => {'attribute-missing' => 'drop'} reader = Asciidoctor::PreprocessorReader.new doc, input assert_nil reader.read_line end test 'line following dropped include is not dropped' do input = <<-EOS include::{foodir}/include-file.asciidoc[] yo EOS doc = empty_safe_document :base_dir => DIRNAME, :attributes => {'attribute-missing' => 'drop'} reader = Asciidoctor::PreprocessorReader.new doc, input assert_equal "yo\n", reader.read_line end test 'escaped include macro is left unprocessed' do input = <<-EOS \\include::fixtures/include-file.asciidoc[] \\escape preserved here EOS doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, input # we should be able to peek it multiple times and still have the backslash preserved # this is the test for @unescape_next_line assert_equal 'include::fixtures/include-file.asciidoc[]', reader.peek_line.chomp assert_equal 'include::fixtures/include-file.asciidoc[]', reader.peek_line.chomp assert_equal 'include::fixtures/include-file.asciidoc[]', reader.read_line.chomp assert_equal '\\escape preserved here', reader.read_line.chomp end test 'include macro not at start of line is ignored' do input = <<-EOS include::include-file.asciidoc[] EOS para = block_from_string input assert_equal 1, para.lines.size # NOTE the space gets stripped because the line is treated as an inline literal assert_equal :literal, para.context assert_equal 'include::include-file.asciidoc[]', para.source end test 'include macro is disabled when max-include-depth attribute is 0' do input = <<-EOS include::include-file.asciidoc[] EOS para = block_from_string input, :safe => :safe, :attributes => { 'max-include-depth' => 0 } assert_equal 1, para.lines.size assert_equal 'include::include-file.asciidoc[]', para.source end test 'max-include-depth cannot be set by document' do input = <<-EOS :max-include-depth: 1 include::include-file.asciidoc[] EOS para = block_from_string input, :safe => :safe, :attributes => { 'max-include-depth' => 0 } assert_equal 1, para.lines.size assert_equal 'include::include-file.asciidoc[]', para.source end test 'include macro should be disabled if max include depth has been exceeded' do input = <<-EOS include::fixtures/parent-include.adoc[depth=1] EOS pseudo_docfile = File.join DIRNAME, 'include-master.adoc' doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, input, Asciidoctor::Reader::Cursor.new(pseudo_docfile) lines = reader.readlines assert lines.include?("include::child-include.adoc[]\n") end test 'include macro should be disabled if max include depth set in nested context has been exceeded' do input = <<-EOS include::fixtures/parent-include-restricted.adoc[depth=3] EOS pseudo_docfile = File.join DIRNAME, 'include-master.adoc' doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, input, Asciidoctor::Reader::Cursor.new(pseudo_docfile) lines = reader.readlines assert lines.include?("first line of child\n") assert lines.include?("include::grandchild-include.adoc[]\n") end test 'read_lines_until should not process lines if process option is false' do lines = <<-EOS.each_line.to_a //// include::fixtures/no-such-file.asciidoc[] //// EOS doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, lines reader.read_line result = reader.read_lines_until(:terminator => '////', :skip_processing => true) assert_equal lines[1..1], result end test 'skip_comment_lines should not process lines read' do lines = <<-EOS.each_line.to_a //// include::fixtures/no-such-file.asciidoc[] //// EOS doc = empty_safe_document :base_dir => DIRNAME reader = Asciidoctor::PreprocessorReader.new doc, lines result = reader.skip_comment_lines assert_equal lines, result end end context 'Conditional Inclusions' do #test 'process_line returns next line of content' do test 'process_line returns nil if cursor advanced' do input = <<-EOS ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] EOS reader = Asciidoctor::PreprocessorReader.new empty_document, input #assert_equal "Asciidoctor!\n", reader.process_line(reader.lines.first) assert_nil reader.process_line(reader.lines.first) end test 'peek_line advances cursor to next conditional line of content' do input = <<-EOS ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] EOS reader = Asciidoctor::PreprocessorReader.new empty_document, input assert_equal 1, reader.lineno assert_equal "Asciidoctor!\n", reader.peek_line assert_equal 2, reader.lineno end test 'process_line returns line if cursor not advanced' do input = <<-EOS content ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] EOS reader = Asciidoctor::PreprocessorReader.new empty_document, input assert_not_nil reader.process_line(reader.lines.first) end test 'peek_line does not advance cursor when on a regular content line' do input = <<-EOS content ifdef::asciidoctor[] Asciidoctor! endif::asciidoctor[] EOS reader = Asciidoctor::PreprocessorReader.new empty_document, input assert_equal 1, reader.lineno assert_equal "content\n", reader.peek_line assert_equal 1, reader.lineno end test 'peek_line returns nil if cursor advances past end of source' do input = <<-EOS ifdef::foobar[] swallowed content endif::foobar[] EOS reader = Asciidoctor::PreprocessorReader.new empty_document, input assert_equal 1, reader.lineno assert_nil reader.peek_line assert_equal 4, reader.lineno end test 'ifdef with defined attribute includes content' do input = <<-EOS ifdef::holygrail[] There is a holy grail! endif::holygrail[] EOS doc = empty_document :attributes => {'holygrail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'There is a holy grail!', lines.join.chomp end test 'ifdef with defined attribute includes text in brackets' do input = <<-EOS On our quest we go... ifdef::holygrail[There is a holy grail!] There was much rejoicing. EOS doc = empty_document :attributes => {'holygrail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "On our quest we go...\nThere is a holy grail!\nThere was much rejoicing.", lines.join.chomp end test 'ifndef with defined attribute does not include text in brackets' do input = <<-EOS On our quest we go... ifndef::hardships[There is a holy grail!] There was no rejoicing. EOS doc = empty_document :attributes => {'hardships' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "On our quest we go...\nThere was no rejoicing.", lines.join.chomp end test 'include with non-matching nested exclude' do input = <<-EOS ifdef::grail[] holy ifdef::swallow[] swallow endif::swallow[] grail endif::grail[] EOS doc = empty_document :attributes => {'grail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "holy\ngrail", lines.join.chomp end test 'nested excludes with same condition' do input = <<-EOS ifndef::grail[] ifndef::grail[] not here endif::grail[] endif::grail[] EOS doc = empty_document :attributes => {'grail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal '', lines.join.chomp end test 'include with nested exclude of inverted condition' do input = <<-EOS ifdef::grail[] holy ifndef::grail[] not here endif::grail[] grail endif::grail[] EOS doc = empty_document :attributes => {'grail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "holy\ngrail", lines.join.chomp end test 'exclude with matching nested exclude' do input = <<-EOS poof ifdef::swallow[] no ifdef::swallow[] swallow endif::swallow[] here endif::swallow[] gone EOS doc = empty_document :attributes => {'grail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "poof\ngone", lines.join.chomp end test 'exclude with nested include using shorthand end' do input = <<-EOS poof ifndef::grail[] no grail ifndef::swallow[] or swallow endif::[] in here endif::[] gone EOS doc = empty_document :attributes => {'grail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "poof\ngone", lines.join.chomp end test 'ifdef with one alternative attribute set includes content' do input = <<-EOS ifdef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] EOS doc = empty_document :attributes => {'swallow' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest is complete!', lines.join.chomp end test 'ifdef with no alternative attributes set does not include content' do input = <<-EOS ifdef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal '', lines.join.chomp end test 'ifdef with all required attributes set includes content' do input = <<-EOS ifdef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] EOS doc = empty_document :attributes => {'holygrail' => '', 'swallow' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest is complete!', lines.join.chomp end test 'ifdef with missing required attributes does not include content' do input = <<-EOS ifdef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] EOS doc = empty_document :attributes => {'holygrail' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal '', lines.join.chomp end test 'ifndef with undefined attribute includes block' do input = <<-EOS ifndef::holygrail[] Our quest continues to find the holy grail! endif::holygrail[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest continues to find the holy grail!', lines.join.chomp end test 'ifndef with one alternative attribute set includes content' do input = <<-EOS ifndef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] EOS doc = empty_document :attributes => {'swallow' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest is complete!', lines.join.chomp end test 'ifndef with no alternative attributes set includes content' do input = <<-EOS ifndef::holygrail,swallow[] Our quest is complete! endif::holygrail,swallow[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest is complete!', lines.join.chomp end test 'ifndef with any required attributes set does not include content' do input = <<-EOS ifndef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] EOS doc = empty_document :attributes => {'swallow' => ''} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal '', lines.join.chomp end test 'ifndef with no required attributes set includes content' do input = <<-EOS ifndef::holygrail+swallow[] Our quest is complete! endif::holygrail+swallow[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Our quest is complete!', lines.join.chomp end test 'escaped ifdef is unescaped and ignored' do input = <<-EOS \\ifdef::holygrail[] content \\endif::holygrail[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "ifdef::holygrail[]\ncontent\nendif::holygrail[]", lines.join.chomp end test 'ifeval comparing double-quoted attribute to matching string is included' do input = <<-EOS ifeval::["{gem}" == "asciidoctor"] Asciidoctor it is! endif::[] EOS doc = empty_document :attributes => {'gem' => 'asciidoctor'} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Asciidoctor it is!', lines.join.chomp end test 'ifeval comparing single-quoted attribute to matching string is included' do input = <<-EOS ifeval::['{gem}' == 'asciidoctor'] Asciidoctor it is! endif::[] EOS doc = empty_document :attributes => {'gem' => 'asciidoctor'} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Asciidoctor it is!', lines.join.chomp end test 'ifeval comparing quoted attribute to non-matching string is ignored' do input = <<-EOS ifeval::['{gem}' == 'asciidoctor'] Asciidoctor it is! endif::[] EOS doc = empty_document :attributes => {'gem' => 'tilt'} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal '', lines.join.chomp end test 'ifeval comparing attribute to lower version number is included' do input = <<-EOS ifeval::['{asciidoctor-version}' >= '0.1.0'] That version will do! endif::[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'That version will do!', lines.join.chomp end test 'ifeval comparing attribute to self is included' do input = <<-EOS ifeval::['{asciidoctor-version}' == '{asciidoctor-version}'] Of course it's the same! endif::[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'Of course it\'s the same!', lines.join.chomp end test 'ifeval arguments can be transposed' do input = <<-EOS ifeval::["0.1.0" <= "{asciidoctor-version}"] That version will do! endif::[] EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'That version will do!', lines.join.chomp end test 'ifeval matching numeric comparison is included' do input = <<-EOS ifeval::[{rings} == 1] One ring to rule them all! endif::[] EOS doc = empty_document :attributes => {'rings' => 1} reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal 'One ring to rule them all!', lines.join.chomp end test 'ifdef with no target is ignored' do input = <<-EOS ifdef::[] content EOS doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input lines = [] while reader.has_more_lines? lines << reader.read_line end assert_equal "ifdef::[]\ncontent", lines.join.chomp end end end end asciidoctor-0.1.4/test/renderer_test.rb000066400000000000000000000165051221220517700201760ustar00rootroot00000000000000require 'test_helper' require 'tilt' context 'Renderer' do context 'View mapping' do test 'should extract view mapping from built-in template with one segment and backend' do view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::HTML5::DocumentTemplate') assert_equal 'document', view_name assert_equal 'html5', view_backend end test 'should extract view mapping from built-in template with two segments and backend' do view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::DocBook45::BlockSidebarTemplate') assert_equal 'block_sidebar', view_name assert_equal 'docbook45', view_backend end test 'should extract view mapping from built-in template without backend' do view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::DocumentTemplate') assert_equal 'document', view_name assert view_backend.nil? end end context 'View options' do test 'should set Haml format to html5 for html5 backend' do doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate assert_equal :html5, doc.renderer.views['block_paragraph'].options[:format] end test 'should set Haml format to xhtml for docbook backend' do doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate assert_equal :xhtml, doc.renderer.views['block_paragraph'].options[:format] end end context 'Custom backends' do test 'should load Haml templates for default backend' do doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.html.haml' assert doc.renderer.views['block_sidebar'].is_a? Tilt::HamlTemplate assert doc.renderer.views['block_sidebar'].file.end_with? 'block_sidebar.html.haml' end test 'should load Haml templates for docbook45 backend' do doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.xml.haml' end test 'should use Haml templates in place of built-in templates' do input = <<-EOS = Document Title Author Name == Section One Sample paragraph .Related **** Sidebar content **** EOS output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1 assert_xpath '//aside', output, 1 assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1 assert_xpath '//aside/header/h1[text()="Related"]', output, 1 assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1 end test 'should use built-in global cache to cache templates' do # clear out any cache, just to be sure Asciidoctor::Renderer.reset_global_cache template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml') doc = Asciidoctor::Document.new [], :template_dir => template_dir doc.renderer template_cache = Asciidoctor::Renderer.global_cache assert template_cache.is_a? Asciidoctor::TemplateCache cache = template_cache.cache assert_not_nil cache assert cache.size > 0 # ensure we don't scan a second time (using the view option hash to mark the cached view object) template_path = Asciidoctor::PathResolver.new.system_path(File.join(template_dir, 'html5', 'block_paragraph.html.haml'), nil) view = template_cache.fetch(:view, template_path) view.options[:foo] = 'bar' doc = Asciidoctor::Document.new [], :template_dir => template_dir doc.renderer template_cache = Asciidoctor::Renderer.global_cache view = template_cache.fetch(:view, template_path) assert_equal 'bar', view.options[:foo] # clean up Asciidoctor::Renderer.reset_global_cache end test 'should use custom cache to cache templates' do template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml') template_path = Asciidoctor::PathResolver.new.system_path(File.join(template_dir, 'html5', 'block_paragraph.html.haml'), nil) doc = Asciidoctor::Document.new [], :template_dir => template_dir, :template_cache => Asciidoctor::TemplateCache.new template_cache = doc.renderer.cache assert_not_nil template_cache cache = template_cache.cache assert_not_nil cache assert cache.size > 0 view = template_cache.fetch(:view, template_path) assert view.is_a? Tilt::HamlTemplate end test 'should be able to disable template cache' do doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false assert_nil doc.renderer.cache end test 'should load Slim templates for default backend' do doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Slim::Template assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.html.slim' assert doc.renderer.views['block_sidebar'].is_a? Slim::Template assert doc.renderer.views['block_sidebar'].file.end_with? 'block_sidebar.html.slim' end test 'should load Slim templates for docbook45 backend' do doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false assert doc.renderer.views['block_paragraph'].is_a? Slim::Template assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.xml.slim' end test 'should use Slim templates in place of built-in templates' do input = <<-EOS = Document Title Author Name == Section One Sample paragraph .Related **** Sidebar content **** EOS output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1 assert_xpath '//aside', output, 1 assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1 assert_xpath '//aside/header/h1[text()="Related"]', output, 1 assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1 end end end asciidoctor-0.1.4/test/sections_test.rb000066400000000000000000001446461221220517700202270ustar00rootroot00000000000000# encoding: UTF-8 require 'test_helper' context 'Sections' do context 'Ids' do test 'synthetic id is generated by default' do sec = block_from_string('== Section One') assert_equal '_section_one', sec.id end test 'synthetic id replaces non-word characters with underscores' do sec = block_from_string("== We're back!") assert_equal '_we_re_back', sec.id end test 'synthetic id removes repeating underscores' do sec = block_from_string('== Section $ One') assert_equal '_section_one', sec.id end test 'synthetic id removes entities' do sec = block_from_string('== Ben & Jerry "Ice Cream Brothers"') assert_equal '_ben_jerry_ice_cream_brothers', sec.id end test 'synthetic id prefix can be customized' do sec = block_from_string(":idprefix: id_\n\n== Section One") assert_equal 'id_section_one', sec.id end test 'synthetic id prefix can be set to blank' do sec = block_from_string(":idprefix:\n\n== Section One") assert_equal 'section_one', sec.id end test 'synthetic id prefix is stripped from beginning of id if set to blank' do sec = block_from_string(":idprefix:\n\n== & More") assert_equal 'more', sec.id end test 'synthetic id separator can be customized' do sec = block_from_string(":idseparator: -\n\n== Section One") assert_equal '_section-one', sec.id end test 'synthetic id separator can be set to blank' do sec = block_from_string(":idseparator:\n\n== Section One") assert_equal '_sectionone', sec.id end test 'synthetic ids can be disabled' do sec = block_from_string(":sectids!:\n\n== Section One\n") assert sec.id.nil? end test 'explicit id in anchor above section title overrides synthetic id' do sec = block_from_string("[[one]]\n== Section One") assert_equal 'one', sec.id end test 'explicit id can be defined using an inline anchor' do sec = block_from_string("== Section One [[one]] ==") assert_equal 'one', sec.id assert_equal 'Section One', sec.title end test 'title substitutions are applied before generating id' do sec = block_from_string("== Section{sp}One\n") assert_equal '_section_one', sec.id end test 'synthetic ids are unique' do input = <<-EOS == Some section text == Some section text EOS doc = document_from_string input assert_equal '_some_section', doc.blocks[0].id assert_equal '_some_section_2', doc.blocks[1].id end end context "document title (level 0)" do test "document title with multiline syntax" do title = "My Title" chars = "=" * title.length assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars) assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars + "\n") end test "document title with multiline syntax, give a char" do title = "My Title" chars = "=" * (title.length + 1) assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars) assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars + "\n") end test "document title with multiline syntax, take a char" do title = "My Title" chars = "=" * (title.length - 1) assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars) assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string(title + "\n" + chars + "\n") end test 'document title with multiline syntax and unicode characters' do input = <<-EOS AsciiDoc Writer’s Guide ======================= Author Name preamble EOS result = render_string input assert_xpath '//h1', result, 1 assert_xpath '//h1[text()="AsciiDoc Writer’s Guide"]', result, 1 end test "not enough chars for a multiline document title" do title = "My Title" chars = "=" * (title.length - 2) assert_xpath '//h1', render_string(title + "\n" + chars), 0 assert_xpath '//h1', render_string(title + "\n" + chars + "\n"), 0 end test "too many chars for a multiline document title" do title = "My Title" chars = "=" * (title.length + 2) assert_xpath '//h1', render_string(title + "\n" + chars), 0 assert_xpath '//h1', render_string(title + "\n" + chars + "\n"), 0 end test "document title with multiline syntax cannot begin with a dot" do title = ".My Title" chars = "=" * title.length assert_xpath '//h1', render_string(title + "\n" + chars), 0 end test "document title with single-line syntax" do assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string("= My Title") end test "document title with symmetric syntax" do assert_xpath "//h1[not(@id)][text() = 'My Title']", render_string("= My Title =") end end context "level 1" do test "with multiline syntax" do assert_xpath "//h2[@id='_my_section'][text() = 'My Section']", render_string("My Section\n-----------") end test "heading title with multiline syntax cannot begin with a dot" do title = ".My Title" chars = "-" * title.length assert_xpath '//h2', render_string(title + "\n" + chars), 0 end test "with single-line syntax" do assert_xpath "//h2[@id='_my_title'][text() = 'My Title']", render_string("== My Title") end test "with single-line symmetric syntax" do assert_xpath "//h2[@id='_my_title'][text() = 'My Title']", render_string("== My Title ==") end test "with single-line non-matching symmetric syntax" do assert_xpath "//h2[@id='_my_title'][text() = 'My Title ===']", render_string("== My Title ===") end test "with non-word character" do assert_xpath "//h2[@id='_where_s_the_love'][text() = \"Where#{[8217].pack('U*')}s the love?\"]", render_string("== Where's the love?") end test "with sequential non-word characters" do assert_xpath "//h2[@id='_what_the_is_this'][text() = 'What the \#@$ is this?']", render_string('== What the #@$ is this?') end test "with trailing whitespace" do assert_xpath "//h2[@id='_my_title'][text() = 'My Title']", render_string("== My Title ") end test "with custom blank idprefix" do assert_xpath "//h2[@id='my_title'][text() = 'My Title']", render_string(":idprefix:\n\n== My Title ") end test "with custom non-blank idprefix" do assert_xpath "//h2[@id='ref_my_title'][text() = 'My Title']", render_string(":idprefix: ref_\n\n== My Title ") end test 'with multibyte characters' do input = <<-EOS == Asciidoctor in 中文 EOS output = render_string input assert_xpath '//h2[@id="_asciidoctor_in"][text()="Asciidoctor in 中文"]', output end end context "level 2" do test "with multiline syntax" do assert_xpath "//h3[@id='_my_section'][text() = 'My Section']", render_string(":fragment:\nMy Section\n~~~~~~~~~~~") end test "with single line syntax" do assert_xpath "//h3[@id='_my_title'][text() = 'My Title']", render_string(":fragment:\n=== My Title") end end context "level 3" do test "with multiline syntax" do assert_xpath "//h4[@id='_my_section'][text() = 'My Section']", render_string(":fragment:\nMy Section\n^^^^^^^^^^") end test "with single line syntax" do assert_xpath "//h4[@id='_my_title'][text() = 'My Title']", render_string(":fragment:\n==== My Title") end end context "level 4" do test "with multiline syntax" do assert_xpath "//h5[@id='_my_section'][text() = 'My Section']", render_string(":fragment:\nMy Section\n++++++++++") end test "with single line syntax" do assert_xpath "//h5[@id='_my_title'][text() = 'My Title']", render_string(":fragment:\n===== My Title") end end context "level 5" do test "with single line syntax" do assert_xpath "//h6[@id='_my_title'][text() = 'My Title']", render_string(":fragment:\n====== My Title") end end context 'Markdown-style headings' do test 'single-line document title with leading marker' do input = <<-EOS # Document Title EOS output = render_string input assert_xpath "//h1[not(@id)][text() = 'Document Title']", output, 1 end test 'single-line document title with symmetric markers' do input = <<-EOS # Document Title # EOS output = render_string input assert_xpath "//h1[not(@id)][text() = 'Document Title']", output, 1 end test 'single-line section title with leading marker' do input = <<-EOS ## Section One blah blah EOS output = render_string input assert_xpath "//h2[@id='_section_one'][text() = 'Section One']", output, 1 end test 'single-line section title with symmetric markers' do input = <<-EOS ## Section One ## blah blah EOS output = render_string input assert_xpath "//h2[@id='_section_one'][text() = 'Section One']", output, 1 end end context 'Floating Title' do test 'should create floating title if style is float' do input = <<-EOS [float] = Plain Ol' Heading not in section EOS output = render_embedded_string input assert_xpath '/h1[@id="_plain_ol_heading"]', output, 1 assert_xpath '/h1[@class="float"]', output, 1 assert_xpath %(/h1[@class="float"][text()="Plain Ol' Heading"]), output, 1 assert_xpath '/h1/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p', output, 1 assert_xpath '/h1/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1 end test 'should create floating title if style is discrete' do input = <<-EOS [discrete] === Plain Ol' Heading not in section EOS output = render_embedded_string input assert_xpath '/h3', output, 1 assert_xpath '/h3[@id="_plain_ol_heading"]', output, 1 assert_xpath '/h3[@class="discrete"]', output, 1 assert_xpath %(/h3[@class="discrete"][text()="Plain Ol' Heading"]), output, 1 assert_xpath '/h3/following-sibling::*[@class="paragraph"]', output, 1 assert_xpath '/h3/following-sibling::*[@class="paragraph"]/p', output, 1 assert_xpath '/h3/following-sibling::*[@class="paragraph"]/p[text()="not in section"]', output, 1 end test 'floating title should be a block with context floating_title' do input = <<-EOS [float] === Plain Ol' Heading not in section EOS doc = document_from_string input floatingtitle = doc.blocks.first assert floatingtitle.is_a?(Asciidoctor::Block) assert !floatingtitle.is_a?(Asciidoctor::Section) assert_equal :floating_title, floatingtitle.context assert_equal '_plain_ol_heading', floatingtitle.id assert doc.references[:ids].has_key?('_plain_ol_heading') end test 'can assign explicit id to floating title' do input = <<-EOS [[unchained]] [float] === Plain Ol' Heading not in section EOS doc = document_from_string input floating_title = doc.blocks.first assert_equal 'unchained', floating_title.id assert doc.references[:ids].has_key?('unchained') end test 'should not include floating title in toc' do input = <<-EOS :toc: == Section One [float] === Miss Independent == Section Two EOS output = render_string input assert_xpath '//*[@id="toc"]', output, 1 assert_xpath %(//*[@id="toc"]//a[contains(text(), "Section ")]), output, 2 assert_xpath %(//*[@id="toc"]//a[text()="Miss Independent"]), output, 0 end test 'should not set id on floating title if sectids attribute is unset' do input = <<-EOS [float] === Plain Ol' Heading not in section EOS output = render_embedded_string input, :attributes => {'sectids' => nil} assert_xpath '/h3', output, 1 assert_xpath '/h3[@id="_plain_ol_heading"]', output, 0 assert_xpath '/h3[@class="float"]', output, 1 end test 'should use explicit id for floating title if specified' do input = <<-EOS [[free]] [float] == Plain Ol' Heading not in section EOS output = render_embedded_string input assert_xpath '/h2', output, 1 assert_xpath '/h2[@id="free"]', output, 1 assert_xpath '/h2[@class="float"]', output, 1 end test 'should add role to class attribute on floating title' do input = <<-EOS [float, role="isolated"] == Plain Ol' Heading not in section EOS output = render_embedded_string input assert_xpath '/h2', output, 1 assert_xpath '/h2[@id="_plain_ol_heading"]', output, 1 assert_xpath '/h2[@class="float isolated"]', output, 1 end end context 'Level offset' do test 'should print error if standalone document is included without level offset' do input = <<-EOS = Master Document Doc Writer text in master // begin simulated include::[] = Standalone Document :author: Junior Writer text in standalone // end simulated include::[] EOS output, errors = nil redirect_streams do |stdout, stderr| output = render_string input errors = stderr.string end assert !errors.empty? assert_match(/only book doctypes can contain level 0 sections/, errors) end test 'should add level offset to section level' do input = <<-EOS = Master Document Doc Writer Master document written by {author}. :leveloffset: 1 // begin simulated include::[] = Standalone Document :author: Junior Writer Standalone document written by {author}. == Section in Standalone Standalone section text. // end simulated include::[] :leveloffset!: == Section in Master Master section text. EOS output = nil errors = nil redirect_streams do |stdout, stderr| output = render_string input errors = stdout.string end assert errors.empty? assert_match(/Master document written by Doc Writer/, output) assert_match(/Standalone document written by Junior Writer/, output) assert_xpath '//*[@class="sect1"]/h2[text() = "Standalone Document"]', output, 1 assert_xpath '//*[@class="sect2"]/h3[text() = "Section in Standalone"]', output, 1 assert_xpath '//*[@class="sect1"]/h2[text() = "Section in Master"]', output, 1 end test 'level offset should be added to floating title' do input = <<-EOS = Master Document Doc Writer :leveloffset: 1 [float] = Floating Title EOS output = render_string input assert_xpath '//h2[@class="float"][text() = "Floating Title"]', output, 1 end test 'should be able to reset level offset' do input = <<-EOS = Master Document Doc Writer Master preamble. :leveloffset: 1 = Standalone Document Standalone preamble. :leveloffset!: == Level 1 Section EOS output = render_string input assert_xpath '//*[@class = "sect1"]/h2[text() = "Standalone Document"]', output, 1 assert_xpath '//*[@class = "sect1"]/h2[text() = "Level 1 Section"]', output, 1 end end context 'Section Numbering' do test 'should create section number with one entry for level 1' do sect1 = Asciidoctor::Section.new assert_equal '1.', sect1.sectnum end test 'should create section number with two entries for level 2' do sect1 = Asciidoctor::Section.new sect1_1 = Asciidoctor::Section.new(sect1) sect1 << sect1_1 assert_equal '1.1.', sect1_1.sectnum end test 'should create section number with three entries for level 3' do sect1 = Asciidoctor::Section.new sect1_1 = Asciidoctor::Section.new(sect1) sect1 << sect1_1 sect1_1_1 = Asciidoctor::Section.new(sect1_1) sect1_1 << sect1_1_1 assert_equal '1.1.1.', sect1_1_1.sectnum end test 'should create section number for second section in level' do sect1 = Asciidoctor::Section.new sect1_1 = Asciidoctor::Section.new(sect1) sect1 << sect1_1 sect1_2 = Asciidoctor::Section.new(sect1) sect1 << sect1_2 assert_equal '1.2.', sect1_2.sectnum end test 'sectnum should use specified delimiter and append string' do sect1 = Asciidoctor::Section.new sect1_1 = Asciidoctor::Section.new(sect1) sect1 << sect1_1 sect1_1_1 = Asciidoctor::Section.new(sect1_1) sect1_1 << sect1_1_1 assert_equal '1,1,1,', sect1_1_1.sectnum(',') assert_equal '1:1:1', sect1_1_1.sectnum(':', false) end test 'should render section numbers when numbered attribute is set' do input = <<-EOS = Title :numbered: == Section_1 text === Section_1_1 text ==== Section_1_1_1 text == Section_2 text === Section_2_1 text === Section_2_2 text EOS output = render_string input assert_xpath '//h2[@id="_section_1"][starts-with(text(), "1. ")]', output, 1 assert_xpath '//h3[@id="_section_1_1"][starts-with(text(), "1.1. ")]', output, 1 assert_xpath '//h4[@id="_section_1_1_1"][starts-with(text(), "1.1.1. ")]', output, 1 assert_xpath '//h2[@id="_section_2"][starts-with(text(), "2. ")]', output, 1 assert_xpath '//h3[@id="_section_2_1"][starts-with(text(), "2.1. ")]', output, 1 assert_xpath '//h3[@id="_section_2_2"][starts-with(text(), "2.2. ")]', output, 1 end test 'blocks should have level' do input = <<-EOS = Title preamble == Section 1 paragraph === Section 1.1 paragraph EOS doc = document_from_string input assert_equal 0, doc.blocks[0].level assert_equal 1, doc.blocks[1].level assert_equal 1, doc.blocks[1].blocks[0].level assert_equal 2, doc.blocks[1].blocks[1].level assert_equal 2, doc.blocks[1].blocks[1].blocks[0].level end test 'section numbers should not increment when numbered attribute is turned off within document' do input = <<-EOS = Document Title :numbered: :numbered!: == Colophon Section == Another Colophon Section == Final Colophon Section :numbered: == Section One === Section One Subsection == Section Two == Section Three EOS output = render_string input assert_xpath '//h1[text()="Document Title"]', output, 1 assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1 assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1 assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1 assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1 assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1 assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1 assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1 end test 'section numbers can be toggled even if numbered attribute is enable via the API' do input = <<-EOS = Document Title :numbered!: == Colophon Section == Another Colophon Section == Final Colophon Section :numbered: == Section One === Section One Subsection == Section Two == Section Three EOS output = render_string input, :attributes => {'numbered' => ''} assert_xpath '//h1[text()="Document Title"]', output, 1 assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1 assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1 assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1 assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1 assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1 assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1 assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1 end test 'section numbers cannot be toggled even if numbered attribute is disabled via the API' do input = <<-EOS = Document Title :numbered!: == Colophon Section == Another Colophon Section == Final Colophon Section :numbered: == Section One === Section One Subsection == Section Two == Section Three EOS output = render_string input, :attributes => {'numbered!' => ''} assert_xpath '//h1[text()="Document Title"]', output, 1 assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1 assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1 assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1 assert_xpath '//h2[@id="_section_one"][text()="Section One"]', output, 1 assert_xpath '//h3[@id="_section_one_subsection"][text()="Section One Subsection"]', output, 1 assert_xpath '//h2[@id="_section_two"][text()="Section Two"]', output, 1 assert_xpath '//h2[@id="_section_three"][text()="Section Three"]', output, 1 end # NOTE AsciiDoc fails this test because it does not properly check for a None value when looking up the numbered attribute test 'section numbers should not increment until numbered attribute is turned back on' do input = <<-EOS = Document Title :numbered!: == Colophon Section == Another Colophon Section == Final Colophon Section :numbered: == Section One === Section One Subsection == Section Two == Section Three EOS output = render_string input assert_xpath '//h1[text()="Document Title"]', output, 1 assert_xpath '//h2[@id="_colophon_section"][text()="Colophon Section"]', output, 1 assert_xpath '//h2[@id="_another_colophon_section"][text()="Another Colophon Section"]', output, 1 assert_xpath '//h2[@id="_final_colophon_section"][text()="Final Colophon Section"]', output, 1 assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1 assert_xpath '//h3[@id="_section_one_subsection"][text()="1.1. Section One Subsection"]', output, 1 assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1 assert_xpath '//h2[@id="_section_three"][text()="3. Section Three"]', output, 1 end test 'table with asciidoc content should not disable numbering of subsequent sections' do input = <<-EOS = Document Title :numbered: preamble == Section One |=== a|content |=== == Section Two content EOS output = render_string input assert_xpath '//h2[@id="_section_one"]', output, 1 assert_xpath '//h2[@id="_section_one"][text()="1. Section One"]', output, 1 assert_xpath '//h2[@id="_section_two"]', output, 1 assert_xpath '//h2[@id="_section_two"][text()="2. Section Two"]', output, 1 end test 'should not number parts when doctype is book' do input = <<-EOS = Document Title :doctype: book :numbered: = Part 1 == Part 1: Chapter 1 = Part 2 == Part 2: Chapter 1 EOS output = render_string input assert_xpath '(//h1)[1][text()="Document Title"]', output, 1 assert_xpath '(//h1)[2][text()="Part 1"]', output, 1 assert_xpath '(//h1)[3][text()="Part 2"]', output, 1 assert_xpath '(//h2)[1][text()="1. Part 1: Chapter 1"]', output, 1 assert_xpath '(//h2)[2][text()="1. Part 2: Chapter 1"]', output, 1 end end context 'Links and anchors' do test 'should include anchor if sectanchors document attribute is set' do input = <<-EOS == Installation Installation section. === Linux Linux installation instructions. EOS output = render_embedded_string input, :attributes => {'sectanchors' => ''} assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a', output, 1 assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[@class="anchor"][@href="#_installation"]', output, 1 assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a/following-sibling::text()="Installation"', output, true assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a', output, 1 assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[@class="anchor"][@href="#_linux"]', output, 1 assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a/following-sibling::text()="Linux"', output, true end test 'should link section if sectlinks document attribute is set' do input = <<-EOS == Installation Installation section. === Linux Linux installation instructions. EOS output = render_embedded_string input, :attributes => {'sectlinks' => ''} assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a', output, 1 assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[@class="link"][@href="#_installation"]', output, 1 assert_xpath '/*[@class="sect1"]/h2[@id="_installation"]/a[text()="Installation"]', output, 1 assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a', output, 1 assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[@class="link"][@href="#_linux"]', output, 1 assert_xpath '//*[@class="sect2"]/h3[@id="_linux"]/a[text()="Linux"]', output, 1 end end context 'Special sections' do test 'should assign sectname and caption to appendix section' do input = <<-EOS [appendix] == Attribute Options Details EOS output = block_from_string input assert_equal 'appendix', output.sectname assert_equal 'Appendix A: ', output.caption end test 'should render appendix title prefixed with caption' do input = <<-EOS [appendix] == Attribute Options Details EOS output = render_embedded_string input assert_xpath '//h2[text()="Appendix A: Attribute Options"]', output, 1 end test 'should increment appendix number for each appendix section' do input = <<-EOS [appendix] == Attribute Options Details [appendix] == Migration Details EOS output = render_embedded_string input assert_xpath '(//h2)[1][text()="Appendix A: Attribute Options"]', output, 1 assert_xpath '(//h2)[2][text()="Appendix B: Migration"]', output, 1 end test 'should not number level 4 section' do input = <<-EOS :numbered: == Level_1 === Level_2 ==== Level_3 ===== Level_4 text EOS output = render_embedded_string input assert_xpath '//h5', output, 1 assert_xpath '//h5[text()="Level_4"]', output, 1 end test 'should not number sections or subsections in regions where numbered is off' do input = <<-EOS :numbered: == Section One :numbered!: [appendix] == Attribute Options Details [appendix] == Migration Details === Gotchas Details [glossary] == Glossary Terms EOS output = render_embedded_string input assert_xpath '(//h2)[1][text()="1. Section One"]', output, 1 assert_xpath '(//h2)[2][text()="Appendix A: Attribute Options"]', output, 1 assert_xpath '(//h2)[3][text()="Appendix B: Migration"]', output, 1 assert_xpath '(//h3)[1][text()="Gotchas"]', output, 1 assert_xpath '(//h2)[4][text()="Glossary"]', output, 1 end test 'should not number sections or subsections in toc in regions where numbered is off' do input = <<-EOS :numbered: :toc: == Section One :numbered!: [appendix] == Attribute Options Details [appendix] == Migration Details === Gotchas Details [glossary] == Glossary Terms EOS output = render_string input assert_xpath '//*[@id="toc"]/ul//li/a[text()="1. Section One"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix A: Attribute Options"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix B: Migration"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Gotchas"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Glossary"]', output, 1 end # reenable once we have :specialnumbered!: implemented =begin test 'should not number special sections or subsections' do input = <<-EOS :numbered: :specialnumbered!: == Section One [appendix] == Attribute Options Details [appendix] == Migration Details === Gotchas Details [glossary] == Glossary Terms EOS output = render_embedded_string input assert_xpath '(//h2)[1][text()="1. Section One"]', output, 1 assert_xpath '(//h2)[2][text()="Appendix A: Attribute Options"]', output, 1 assert_xpath '(//h2)[3][text()="Appendix B: Migration"]', output, 1 assert_xpath '(//h3)[1][text()="Gotchas"]', output, 1 assert_xpath '(//h2)[4][text()="Glossary"]', output, 1 end test 'should not number special sections or subsections in toc' do input = <<-EOS :numbered: :specialnumbered!: :toc: == Section One [appendix] == Attribute Options Details [appendix] == Migration Details === Gotchas Details [glossary] == Glossary Terms EOS output = render_string input assert_xpath '//*[@id="toc"]/ul//li/a[text()="1. Section One"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix A: Attribute Options"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Appendix B: Migration"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Gotchas"]', output, 1 assert_xpath '//*[@id="toc"]/ul//li/a[text()="Glossary"]', output, 1 end =end test 'level 0 special sections in multipart book should be rendered as level 1' do input = <<-EOS = Multipart Book Doc Writer :doctype: book [preface] = Preface Preface text [appendix] = Appendix Appendix text EOS output = render_string input assert_xpath '//h2[@id = "_preface"]', output, 1 assert_xpath '//h2[@id = "_appendix"]', output, 1 end test 'should output docbook elements that coorespond to special sections in book doctype' do input = <<-EOS = Multipart Book :doctype: book :idprefix: [abstract] = Abstract Title Normal chapter (no abstract in book) [dedication] = Dedication Title Dedication content [preface] = Preface Title Preface content === Preface sub-section Preface subsection content = Part 1 [partintro] .Part intro title Part intro content == Chapter 1 blah blah == Chapter 2 blah blah = Part 2 blah blah == Chapter 3 blah blah == Chapter 4 blah blah [appendix] = Appendix Title Appendix content === Appendix sub-section Appendix sub-section content [bibliography] = Bibliography Title Bibliography content [glossary] = Glossary Title Glossary content [colophon] = Colophon Title Colophon content [index] = Index Title EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/chapter[@id="abstract_title"]', output, 1 assert_xpath '/chapter[@id="abstract_title"]/title[text()="Abstract Title"]', output, 1 assert_xpath '/chapter/following-sibling::dedication[@id="dedication_title"]', output, 1 assert_xpath '/chapter/following-sibling::dedication[@id="dedication_title"]/title[text()="Dedication Title"]', output, 1 assert_xpath '/dedication/following-sibling::preface[@id="preface_title"]', output, 1 assert_xpath '/dedication/following-sibling::preface[@id="preface_title"]/title[text()="Preface Title"]', output, 1 assert_xpath '/preface/section[@id="preface_sub_section"]', output, 1 assert_xpath '/preface/section[@id="preface_sub_section"]/title[text()="Preface sub-section"]', output, 1 assert_xpath '/preface/following-sibling::part[@id="part_1"]', output, 1 assert_xpath '/preface/following-sibling::part[@id="part_1"]/title[text()="Part 1"]', output, 1 assert_xpath '/part[@id="part_1"]/partintro', output, 1 assert_xpath '/part[@id="part_1"]/partintro/title[text()="Part intro title"]', output, 1 assert_xpath '/part[@id="part_1"]/partintro/following-sibling::chapter[@id="chapter_1"]', output, 1 assert_xpath '/part[@id="part_1"]/partintro/following-sibling::chapter[@id="chapter_1"]/title[text()="Chapter 1"]', output, 1 assert_xpath '(/part)[2]/following-sibling::appendix[@id="appendix_title"]', output, 1 assert_xpath '(/part)[2]/following-sibling::appendix[@id="appendix_title"]/title[text()="Appendix Title"]', output, 1 assert_xpath '/appendix/section[@id="appendix_sub_section"]', output, 1 assert_xpath '/appendix/section[@id="appendix_sub_section"]/title[text()="Appendix sub-section"]', output, 1 assert_xpath '/appendix/following-sibling::bibliography[@id="bibliography_title"]', output, 1 assert_xpath '/appendix/following-sibling::bibliography[@id="bibliography_title"]/title[text()="Bibliography Title"]', output, 1 assert_xpath '/bibliography/following-sibling::glossary[@id="glossary_title"]', output, 1 assert_xpath '/bibliography/following-sibling::glossary[@id="glossary_title"]/title[text()="Glossary Title"]', output, 1 assert_xpath '/glossary/following-sibling::colophon[@id="colophon_title"]', output, 1 assert_xpath '/glossary/following-sibling::colophon[@id="colophon_title"]/title[text()="Colophon Title"]', output, 1 assert_xpath '/colophon/following-sibling::index[@id="index_title"]', output, 1 assert_xpath '/colophon/following-sibling::index[@id="index_title"]/title[text()="Index Title"]', output, 1 end test 'abstract section maps to abstract element in docbook for article doctype' do input = <<-EOS = Article :idprefix: [abstract] == Abstract Title Abstract content EOS output = render_embedded_string input, :backend => 'docbook' assert_xpath '/abstract[@id="abstract_title"]', output, 1 assert_xpath '/abstract[@id="abstract_title"]/title[text()="Abstract Title"]', output, 1 end end context "heading patterns in blocks" do test "should not interpret a listing block as a heading" do input = <<-EOS Section ------- ---- code ---- fin. EOS output = render_string input assert_xpath "//h2", output, 1 end test "should not interpret an open block as a heading" do input = <<-EOS Section ------- -- ha -- fin. EOS output = render_string input assert_xpath "//h2", output, 1 end test "should not interpret an attribute list as a heading" do input = <<-EOS Section ======= preamble [TIP] ==== This should be a tip, not a heading. ==== EOS output = render_string input assert_xpath "//*[@class='admonitionblock tip']//p[text() = 'This should be a tip, not a heading.']", output, 1 end test "should not match a heading in a labeled list" do input = <<-EOS Section ------- term1:: + ---- list = [1, 2, 3]; ---- term2:: == not a heading term3:: def // fin. EOS output = render_string input assert_xpath "//h2", output, 1 assert_xpath "//dl", output, 1 end test "should not match a heading in a bulleted list" do input = <<-EOS Section ------- * first + ---- list = [1, 2, 3]; ---- + * second == not a heading * third fin. EOS output = render_string input assert_xpath "//h2", output, 1 assert_xpath "//ul", output, 1 end test "should not match a heading in a block" do input = <<-EOS ==== == not a heading ==== EOS output = render_string input assert_xpath "//h2", output, 0 assert_xpath "//*[@class='exampleblock']//p[text() = '== not a heading']", output, 1 end end context 'Table of Contents' do test 'should render unnumbered table of contents in header if toc attribute is set' do input = <<-EOS = Article :toc: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... === Interlude While they were waiting... == Section Three That's all she wrote! EOS output = render_string input assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul[@class="sectlevel1"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li', output, 4 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="Section One"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul[@class="sectlevel2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li/a[@href="#_interlude"][text()="Interlude"]', output, 1 assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[4]/a[@href="#_section_three"][text()="Section Three"]', output, 1 end test 'should render numbered table of contents in header if toc and numbered attributes are set' do input = <<-EOS = Article :toc: :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... === Interlude While they were waiting... == Section Three That's all she wrote! EOS output = render_string input assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li', output, 4 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li/ul/li/a[@href="#_interlude"][text()="2.1. Interlude"]', output, 1 assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[4]/a[@href="#_section_three"][text()="3. Section Three"]', output, 1 end test 'should render a table of contents that honors numbered setting at position of section in document' do input = <<-EOS = Article :toc: :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... === Interlude While they were waiting... :numbered!: == Section Three That's all she wrote! EOS output = render_string input assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Table of Contents"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]//ul', output, 2 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li', output, 4 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 assert_xpath '((//*[@id="header"]//*[@id="toc"]/ul)[1]/li)[4]/a[@href="#_section_three"][text()="Section Three"]', output, 1 end test 'should not number parts in table of contents for book doctype when numbered attribute is set' do input = <<-EOS = Book :doctype: book :toc: :numbered: = Part 1 == First Section of Part 1 blah == Second Section of Part 1 blah = Part 2 == First Section of Part 2 blah EOS output = render_string input assert_xpath '//*[@id="toc"]', output, 1 assert_xpath '//*[@id="toc"]/ul', output, 1 assert_xpath '//*[@id="toc"]/ul[@class="sectlevel0"]', output, 1 assert_xpath '//*[@id="toc"]/ul[@class="sectlevel0"]/li', output, 4 assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[1]/a[text()="Part 1"]', output, 1 assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[3]/a[text()="Part 2"]', output, 1 assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[2]/ul', output, 1 assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[2]/ul[@class="sectlevel1"]', output, 1 assert_xpath '(//*[@id="toc"]/ul[@class="sectlevel0"]/li)[2]/ul/li', output, 2 assert_xpath '((//*[@id="toc"]/ul[@class="sectlevel0"]/li)[2]/ul/li)[1]/a[text()="1. First Section of Part 1"]', output, 1 end test 'should render table of contents in header if toc2 attribute is set' do input = <<-EOS = Article :toc2: :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//body[@class="article toc2 toc-left"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 end test 'should set toc position if toc2 attribute is set to position' do input = <<-EOS = Article :toc2: > :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//body[@class="article toc2 toc-right"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 end test 'should set toc position if toc and toc-position attributes are set' do input = <<-EOS = Article :toc: :toc-position: right :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//body[@class="article toc2 toc-right"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 end test 'should set toc position if toc2 and toc-position attribute are set' do input = <<-EOS = Article :toc2: :toc-position: right :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//body[@class="article toc2 toc-right"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 end test 'should set toc position if toc attribute is set to direction' do input = <<-EOS = Article :toc: right :numbered: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//body[@class="article toc2 toc-right"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"][@class="toc2"]', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1 end test 'should use document attributes toc-class, toc-title and toclevels to create toc' do input = <<-EOS = Article :toc: :toc-title: Contents :toc-class: toc2 :toclevels: 1 == Section 1 === Section 1.1 ==== Section 1.1.1 ==== Section 1.1.2 === Section 1.2 == Section 2 Fin. EOS output = render_string input assert_css '#header #toc', output, 1 assert_css '#header #toc.toc2', output, 1 assert_css '#header #toc li', output, 2 assert_css '#header #toc #toctitle', output, 1 assert_xpath '//*[@id="header"]//*[@id="toc"]/*[@id="toctitle"][text()="Contents"]', output, 1 end test 'should not render table of contents if toc-placement attribute is unset' do input = <<-EOS = Article :toc: :toc-placement!: == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_xpath '//*[@id="toc"]', output, 0 end test 'should render table of contents at location of toc macro' do input = <<-EOS = Article :toc: :toc-placement!: Once upon a time... toc::[] == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_css '#preamble #toc', output, 1 assert_css '#preamble .paragraph + #toc', output, 1 end test 'should render table of contents at location of toc macro in embedded document' do input = <<-EOS = Article :toc: :toc-placement!: Once upon a time... toc::[] == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input, :header_footer => false assert_css '#preamble:root #toc', output, 1 assert_css '#preamble:root .paragraph + #toc', output, 1 end test 'should not assign toc id to more than one toc' do input = <<-EOS = Article :toc: Once upon a time... toc::[] == Section One It was a dark and stormy night... == Section Two They couldn't believe their eyes when... EOS output = render_string input assert_css '#toc', output, 1 assert_css '#toctitle', output, 1 assert_xpath '(//*[@class="toc"])[2][not(@id)]', output, 1 assert_xpath '(//*[@class="toc"])[2]/*[@class="title"][not(@id)]', output, 1 end test 'should use global attributes for toc-title, toc-class and toclevels for toc macro' do input = <<-EOS = Article :toc: :toc-placement!: :toc-title: Contents :toc-class: contents :toclevels: 1 Preamble. toc::[] == Section 1 === Section 1.1 ==== Section 1.1.1 ==== Section 1.1.2 === Section 1.2 == Section 2 Fin. EOS output = render_string input assert_css '#toc', output, 1 assert_css '#toctitle', output, 1 assert_css '#preamble #toc', output, 1 assert_css '#preamble #toc.contents', output, 1 assert_xpath '//*[@id="toc"]/*[@class="title"][text() = "Contents"]', output, 1 assert_css '#toc li', output, 2 assert_xpath '(//*[@id="toc"]//li)[1]/a[text() = "Section 1"]', output, 1 assert_xpath '(//*[@id="toc"]//li)[2]/a[text() = "Section 2"]', output, 1 end test 'should honor id, title, role and level attributes on toc macro' do input = <<-EOS = Article :toc: :toc-placement!: :toc-title: Ignored :toc-class: ignored :toclevels: 5 :tocdepth: 1 Preamble. [[contents]] [role="contents"] .Contents toc::[levels={tocdepth}] == Section 1 === Section 1.1 ==== Section 1.1.1 ==== Section 1.1.2 === Section 1.2 == Section 2 Fin. EOS output = render_string input assert_css '#toc', output, 0 assert_css '#toctitle', output, 0 assert_css '#preamble #contents', output, 1 assert_css '#preamble #contents.contents', output, 1 assert_xpath '//*[@id="contents"]/*[@class="title"][text() = "Contents"]', output, 1 assert_css '#contents li', output, 2 assert_xpath '(//*[@id="contents"]//li)[1]/a[text() = "Section 1"]', output, 1 assert_xpath '(//*[@id="contents"]//li)[2]/a[text() = "Section 2"]', output, 1 end end context 'article doctype' do test 'should create sections only in docbook backend' do input = <<-EOS = Article Doc Writer == Section 1 The adventure. === Subsection One It was a dark and stormy night... === Subsection Two They couldn't believe their eyes when... == Section 2 The return. === Subsection Three While they were returning... === Subsection Four That's all she wrote! EOS output = render_string input, :backend => 'docbook' assert_xpath '//part', output, 0 assert_xpath '//chapter', output, 0 assert_xpath '/article/section', output, 2 assert_xpath '/article/section[1]/title[text() = "Section 1"]', output, 1 assert_xpath '/article/section[2]/title[text() = "Section 2"]', output, 1 assert_xpath '/article/section/section', output, 4 assert_xpath '/article/section[1]/section[1]/title[text() = "Subsection One"]', output, 1 assert_xpath '/article/section[2]/section[1]/title[text() = "Subsection Three"]', output, 1 end end context 'book doctype' do test 'document title with level 0 headings' do input = <<-EOS = Book Doc Writer :doctype: book = Chapter One It was a dark and stormy night... = Chapter Two They couldn't believe their eyes when... == Interlude While they were waiting... = Chapter Three That's all she wrote! EOS output = render_string(input) assert_css 'body.book', output, 1 assert_css 'h1', output, 4 assert_css '#header h1', output, 1 assert_css '#content h1', output, 3 assert_css '#content h1.sect0', output, 3 assert_css 'h2', output, 1 assert_css '#content h2', output, 1 assert_xpath '//h1[@id="_chapter_one"][text() = "Chapter One"]', output, 1 assert_xpath '//h1[@id="_chapter_two"][text() = "Chapter Two"]', output, 1 assert_xpath '//h1[@id="_chapter_three"][text() = "Chapter Three"]', output, 1 end test 'should create parts and chapters in docbook backend' do input = <<-EOS = Book Doc Writer :doctype: book = Part 1 The adventure. == Chapter One It was a dark and stormy night... == Chapter Two They couldn't believe their eyes when... = Part 2 The return. == Chapter Three While they were returning... == Chapter Four That's all she wrote! EOS output = render_string input, :backend => 'docbook' assert_xpath '//chapter/chapter', output, 0 assert_xpath '/book/part', output, 2 assert_xpath '/book/part[1]/title[text() = "Part 1"]', output, 1 assert_xpath '/book/part[2]/title[text() = "Part 2"]', output, 1 assert_xpath '/book/part/chapter', output, 4 assert_xpath '/book/part[1]/chapter[1]/title[text() = "Chapter One"]', output, 1 assert_xpath '/book/part[2]/chapter[1]/title[text() = "Chapter Three"]', output, 1 end test 'subsections in preface and appendix should start at level 2' do input = <<-EOS = Multipart Book Doc Writer :doctype: book [preface] = Preface Preface content === Preface subsection Preface subsection content = Part 1 .Part intro title [partintro] Part intro content [appendix] = Appendix Appendix content === Appendix subsection Appendix subsection content EOS output = nil errors = nil redirect_streams do |stdout, stderr| output = render_string input, :backend => 'docbook' errors = stdout.string end assert errors.empty? assert_xpath '/book/preface', output, 1 assert_xpath '/book/preface/section', output, 1 assert_xpath '/book/part', output, 1 assert_xpath '/book/part/partintro', output, 1 assert_xpath '/book/part/partintro/title', output, 1 assert_xpath '/book/part/partintro/simpara', output, 1 assert_xpath '/book/appendix', output, 1 assert_xpath '/book/appendix/section', output, 1 end end end asciidoctor-0.1.4/test/substitutions_test.rb000066400000000000000000001561521221220517700213320ustar00rootroot00000000000000require 'test_helper' # TODO # - test negatives # - test role on every quote type context 'Substitutions' do context 'Dispatcher' do test 'apply normal substitutions' do para = block_from_string("[blue]'http://asciidoc.org[AsciiDoc]' & [red]*Ruby*\n§ Making +++documentation+++ together +\nsince (C) {inception_year}.") para.document.attributes['inception_year'] = '2012' result = para.apply_normal_subs(para.lines) assert_equal %{AsciiDoc & Ruby\n§ Making documentation together
    \nsince © 2012.}, result end end context 'Quotes' do test 'single-line double-quoted string' do para = block_from_string(%q{``a few quoted words''}) assert_equal '“a few quoted words”', para.sub_quotes(para.source) end test 'escaped single-line double-quoted string' do para = block_from_string(%q{\``a few quoted words''}) assert_equal %q(‘`a few quoted words’'), para.sub_quotes(para.source) end test 'multi-line double-quoted string' do para = block_from_string(%Q{``a few\nquoted words''}) assert_equal "“a few\nquoted words”", para.sub_quotes(para.source) end test 'double-quoted string with inline single quote' do para = block_from_string(%q{``Here's Johnny!''}) assert_equal %q{“Here's Johnny!”}, para.sub_quotes(para.source) end test 'double-quoted string with inline backquote' do para = block_from_string(%q{``Here`s Johnny!''}) assert_equal %q{“Here`s Johnny!”}, para.sub_quotes(para.source) end test 'single-line single-quoted string' do para = block_from_string(%q{`a few quoted words'}) assert_equal '‘a few quoted words’', para.sub_quotes(para.source) end test 'escaped single-line single-quoted string' do para = block_from_string(%q{\`a few quoted words'}) assert_equal %(`a few quoted words'), para.sub_quotes(para.source) end test 'multi-line single-quoted string' do para = block_from_string(%Q{`a few\nquoted words'}) assert_equal "‘a few\nquoted words’", para.sub_quotes(para.source) end test 'single-quoted string with inline single quote' do para = block_from_string(%q{`That isn't what I did.'}) assert_equal %q{‘That isn't what I did.’}, para.sub_quotes(para.source) end test 'single-quoted string with inline backquote' do para = block_from_string(%q{`Here`s Johnny!'}) assert_equal %q{‘Here`s Johnny!’}, para.sub_quotes(para.source) end test 'single-line constrained unquoted string' do para = block_from_string(%q{#a few words#}) assert_equal 'a few words', para.sub_quotes(para.source) end test 'escaped single-line constrained unquoted string' do para = block_from_string(%q{\#a few words#}) assert_equal '#a few words#', para.sub_quotes(para.source) end test 'multi-line constrained unquoted string' do para = block_from_string(%Q{#a few\nwords#}) assert_equal "a few\nwords", para.sub_quotes(para.source) end test 'single-line unconstrained unquoted string' do para = block_from_string(%q{##--anything goes ##}) assert_equal '--anything goes ', para.sub_quotes(para.source) end test 'escaped single-line unconstrained unquoted string' do para = block_from_string(%q{\##--anything goes ##}) assert_equal '#--anything goes #', para.sub_quotes(para.source) end test 'multi-line unconstrained unquoted string' do para = block_from_string(%Q{##--anything\ngoes ##}) assert_equal "--anything\ngoes ", para.sub_quotes(para.source) end test 'single-line constrained strong string' do para = block_from_string(%q{*a few strong words*}) assert_equal 'a few strong words', para.sub_quotes(para.source) end test 'escaped single-line constrained strong string' do para = block_from_string(%q{\*a few strong words*}) assert_equal '*a few strong words*', para.sub_quotes(para.source) end test 'multi-line constrained strong string' do para = block_from_string(%Q{*a few\nstrong words*}) assert_equal "a few\nstrong words", para.sub_quotes(para.source) end test 'constrained strong string containing an asterisk' do para = block_from_string(%q{*bl*ck*-eye}) assert_equal 'bl*ck-eye', para.sub_quotes(para.source) end test 'single-line constrained quote variation emphasized string' do para = block_from_string(%q{'a few emphasized words'}) assert_equal 'a few emphasized words', para.sub_quotes(para.source) end test 'escaped single-line constrained quote variation emphasized string' do para = block_from_string(%q{\'a few emphasized words'}) assert_equal %q('a few emphasized words'), para.sub_quotes(para.source) end test 'multi-line constrained emphasized quote variation string' do para = block_from_string(%Q{'a few\nemphasized words'}) assert_equal "a few\nemphasized words", para.sub_quotes(para.source) end test 'single-quoted string containing an emphasized phrase' do para = block_from_string(%q{`I told him, 'Just go for it!''}) assert_equal '‘I told him, Just go for it!’', para.sub_quotes(para.source) end test 'escaped single-quotes inside emphasized words are restored' do para = block_from_string(%q{'Here\'s Johnny!'}) # NOTE the \' is replaced with ' by the :replacements substitution, later in the substitution pipeline assert_equal %q{Here\'s Johnny!}, para.sub_quotes(para.source) end test 'single-line constrained emphasized underline variation string' do para = block_from_string(%q{_a few emphasized words_}) assert_equal 'a few emphasized words', para.sub_quotes(para.source) end test 'escaped single-line constrained emphasized underline variation string' do para = block_from_string(%q{\_a few emphasized words_}) assert_equal '_a few emphasized words_', para.sub_quotes(para.source) end test 'multi-line constrained emphasized underline variation string' do para = block_from_string(%Q{_a few\nemphasized words_}) assert_equal "a few\nemphasized words", para.sub_quotes(para.source) end test 'single-line constrained monospaced string' do para = block_from_string(%q{`a few <\{monospaced\}> words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal 'a few <{monospaced}> words', para.apply_normal_subs(para.lines) end test 'single-line constrained monospaced string with role' do para = block_from_string(%q{[input]`a few <\{monospaced\}> words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal 'a few <{monospaced}> words', para.apply_normal_subs(para.lines) end test 'escaped single-line constrained monospaced string' do para = block_from_string(%q{\`a few words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal '`a few <monospaced> words`', para.apply_normal_subs(para.lines) end test 'escaped single-line constrained monospaced string with role' do para = block_from_string(%q{[input]\`a few words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal '[input]`a few <monospaced> words`', para.apply_normal_subs(para.lines) end test 'escaped role on single-line constrained monospaced string' do para = block_from_string(%q{\[input]`a few words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal '[input]a few <monospaced> words', para.apply_normal_subs(para.lines) end test 'escaped role on escaped single-line constrained monospaced string' do para = block_from_string(%q{\[input]\`a few words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal '\[input]`a few <monospaced> words`', para.apply_normal_subs(para.lines) end test 'multi-line constrained monospaced string' do para = block_from_string(%Q{`a few\n<\{monospaced\}> words`}) # NOTE must use apply_normal_subs because constrained monospaced is handled as a passthrough assert_equal "a few\n<{monospaced}> words", para.apply_normal_subs(para.lines) end test 'single-line unconstrained strong chars' do para = block_from_string(%q{**Git**Hub}) assert_equal 'GitHub', para.sub_quotes(para.source) end test 'escaped single-line unconstrained strong chars' do para = block_from_string(%q{\**Git**Hub}) assert_equal '*Git*Hub', para.sub_quotes(para.source) end test 'multi-line unconstrained strong chars' do para = block_from_string(%Q{**G\ni\nt\n**Hub}) assert_equal "G\ni\nt\nHub", para.sub_quotes(para.source) end test 'unconstrained strong chars with inline asterisk' do para = block_from_string(%q{**bl*ck**-eye}) assert_equal 'bl*ck-eye', para.sub_quotes(para.source) end test 'unconstrained strong chars with role' do para = block_from_string(%q{Git[blue]**Hub**}) assert_equal %q{GitHub}, para.sub_quotes(para.source) end # TODO this is not the same result as AsciiDoc, though I don't understand why AsciiDoc gets what it gets test 'escaped unconstrained strong chars with role' do para = block_from_string(%q{Git\[blue]**Hub**}) assert_equal %q{Git[blue]*Hub*}, para.sub_quotes(para.source) end test 'single-line unconstrained emphasized chars' do para = block_from_string(%q{__Git__Hub}) assert_equal 'GitHub', para.sub_quotes(para.source) end test 'escaped single-line unconstrained emphasized chars' do para = block_from_string(%q{\__Git__Hub}) assert_equal '__Git__Hub', para.sub_quotes(para.source) end test 'multi-line unconstrained emphasized chars' do para = block_from_string(%Q{__G\ni\nt\n__Hub}) assert_equal "G\ni\nt\nHub", para.sub_quotes(para.source) end test 'unconstrained emphasis chars with role' do para = block_from_string(%q{[gray]__Git__Hub}) assert_equal %q{GitHub}, para.sub_quotes(para.source) end test 'escaped unconstrained emphasis chars with role' do para = block_from_string(%q{\[gray]__Git__Hub}) assert_equal %q{[gray]__Git__Hub}, para.sub_quotes(para.source) end test 'single-line constrained monospaced chars' do para = block_from_string(%q{call +save()+ to persist the changes}) assert_equal 'call save() to persist the changes', para.sub_quotes(para.source) end test 'single-line constrained monospaced chars with role' do para = block_from_string(%q{call [method]+save()+ to persist the changes}) assert_equal 'call save() to persist the changes', para.sub_quotes(para.source) end test 'escaped single-line constrained monospaced chars' do para = block_from_string(%q{call \+save()+ to persist the changes}) assert_equal 'call +save()+ to persist the changes', para.sub_quotes(para.source) end test 'escaped single-line constrained monospaced chars with role' do para = block_from_string(%q{call [method]\+save()+ to persist the changes}) assert_equal 'call [method]+save()+ to persist the changes', para.sub_quotes(para.source) end test 'escaped role on single-line constrained monospaced chars' do para = block_from_string(%q{call \[method]+save()+ to persist the changes}) assert_equal 'call [method]save() to persist the changes', para.sub_quotes(para.source) end test 'escaped role on escaped single-line constrained monospaced chars' do para = block_from_string(%q{call \[method]\+save()+ to persist the changes}) assert_equal 'call \[method]+save()+ to persist the changes', para.sub_quotes(para.source) end test 'single-line unconstrained monospaced chars' do para = block_from_string(%q{Git++Hub++}) assert_equal 'GitHub', para.sub_quotes(para.source) end test 'escaped single-line unconstrained monospaced chars' do para = block_from_string(%q{Git\++Hub++}) assert_equal 'Git+Hub+', para.sub_quotes(para.source) end test 'multi-line unconstrained monospaced chars' do para = block_from_string(%Q{Git++\nH\nu\nb++}) assert_equal "Git\nH\nu\nb", para.sub_quotes(para.source) end test 'single-line superscript chars' do para = block_from_string(%q{x^2^ = x * x, e = mc^2^, there's a 1^st^ time for everything}) assert_equal 'x2 = x * x, e = mc2, there\'s a 1st time for everything', para.sub_quotes(para.source) end test 'escaped single-line superscript chars' do para = block_from_string(%q{x\^2^ = x * x}) assert_equal 'x^2^ = x * x', para.sub_quotes(para.source) end test 'multi-line superscript chars' do para = block_from_string(%Q{x^(n\n-\n1)^}) assert_equal "x(n\n-\n1)", para.sub_quotes(para.source) end test 'single-line subscript chars' do para = block_from_string(%q{H~2~O}) assert_equal 'H2O', para.sub_quotes(para.source) end test 'escaped single-line subscript chars' do para = block_from_string(%q{H\~2~O}) assert_equal 'H~2~O', para.sub_quotes(para.source) end test 'multi-line subscript chars' do para = block_from_string(%Q{project~ view\non\nGitHub~}) assert_equal "project view\non\nGitHub", para.sub_quotes(para.source) end test 'quoted text with role shorthand' do para = block_from_string(%q{[.white.red-background]#alert#}) assert_equal 'alert', para.sub_quotes(para.source) end test 'quoted text with id shorthand' do para = block_from_string(%q{[#bond]#007#}) assert_equal '007', para.sub_quotes(para.source) end test 'quoted text with id and role shorthand' do para = block_from_string(%q{[#bond.white.red-background]#007#}) assert_equal '007', para.sub_quotes(para.source) end test 'quoted text with id and role shorthand using docbook backend' do para = block_from_string(%q{[#bond.white.red-background]#007#}, :backend => 'docbook') assert_equal '007', para.sub_quotes(para.source) end test 'should ignore attributes after comma' do para = block_from_string(%q{[red, foobar]#alert#}) assert_equal 'alert', para.sub_quotes(para.source) end end context 'Macros' do test 'a single-line link macro should be interpreted as a link' do para = block_from_string('link:/home.html[]') assert_equal %q{/home.html}, para.sub_macros(para.source) end test 'a single-line link macro with text should be interpreted as a link' do para = block_from_string('link:/home.html[Home]') assert_equal %q{Home}, para.sub_macros(para.source) end test 'a mailto macro should be interpreted as a mailto link' do para = block_from_string('mailto:doc.writer@asciidoc.org[]') assert_equal %q{doc.writer@asciidoc.org}, para.sub_macros(para.source) end test 'a mailto macro with text should be interpreted as a mailto link' do para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer]') assert_equal %q{Doc Writer}, para.sub_macros(para.source) end test 'a mailto macro with text and subject should be interpreted as a mailto link' do para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer, Pull request]', :attributes => {'linkattrs' => ''}) assert_equal %q{Doc Writer}, para.sub_macros(para.source) end test 'a mailto macro with text, subject and body should be interpreted as a mailto link' do para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer, Pull request, Please accept my pull request]', :attributes => {'linkattrs' => ''}) assert_equal %q{Doc Writer}, para.sub_macros(para.source) end test 'should recognize inline email addresses' do para = block_from_string('doc.writer@asciidoc.org') assert_equal %q{doc.writer@asciidoc.org}, para.sub_macros(para.source) para = block_from_string('') assert_equal %q{<doc.writer@asciidoc.org>}, para.apply_normal_subs(para.lines) para = block_from_string('author+website@4fs.no') assert_equal %q{author+website@4fs.no}, para.sub_macros(para.source) para = block_from_string('john@domain.uk.co') assert_equal %q{john@domain.uk.co}, para.sub_macros(para.source) end test 'should ignore escaped inline email address' do para = block_from_string('\doc.writer@asciidoc.org') assert_equal %q{doc.writer@asciidoc.org}, para.sub_macros(para.source) end test 'a single-line raw url should be interpreted as a link' do para = block_from_string('http://google.com') assert_equal %q{http://google.com}, para.sub_macros(para.source) end test 'a single-line raw url with text should be interpreted as a link' do para = block_from_string('http://google.com[Google]') assert_equal %q{Google}, para.sub_macros(para.source) end test 'a multi-line raw url with text should be interpreted as a link' do para = block_from_string("http://google.com[Google\nHomepage]") assert_equal %{Google\nHomepage}, para.sub_macros(para.source) end test 'a multi-line raw url with attribute as text should be interpreted as a link with resolved attribute' do para = block_from_string("http://google.com[{google_homepage}]") para.document.attributes['google_homepage'] = 'Google Homepage' assert_equal %q{Google Homepage}, para.sub_macros(para.source) end test 'a single-line escaped raw url should not be interpreted as a link' do para = block_from_string('\http://google.com') assert_equal %q{http://google.com}, para.sub_macros(para.source) end test 'a comma separated list of links should not include commas in links' do para = block_from_string('http://foo.com, http://bar.com, http://example.org') assert_equal %q{http://foo.com, http://bar.com, http://example.org}, para.sub_macros(para.source) end test 'a single-line image macro should be interpreted as an image' do para = block_from_string('image:tiger.png[]') assert_equal %{tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a single-line image macro with text should be interpreted as an image with alt text' do para = block_from_string('image:tiger.png[Tiger]') assert_equal %{Tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a single-line image macro with text containing escaped square bracket should be interpreted as an image with alt text' do para = block_from_string('image:tiger.png[[Another\] Tiger]') assert_equal %{[Another] Tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a single-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do para = block_from_string('image:tiger.png[Tiger, 200, 100]') assert_equal %{Tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a single-line image macro with text and link should be interpreted as a linked image with alt text' do para = block_from_string('image:tiger.png[Tiger, link="http://en.wikipedia.org/wiki/Tiger"]') assert_equal %{Tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a multi-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do para = block_from_string(%(image:tiger.png[Another\nAwesome\nTiger, 200,\n100])) assert_equal %{Another Awesome Tiger}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an inline image macro with a url target should be interpreted as an image' do para = block_from_string %(Beware of the image:http://example.com/images/tiger.png[tiger].) assert_equal %{Beware of the tiger.}, para.sub_macros(para.source).gsub(/>\s+<') end test 'wip an inline image macro with a float attribute should be interpreted as a floating image' do para = block_from_string %(image:http://example.com/images/tiger.png[tiger, float="right"] Beware of the tigers!) assert_equal %{tiger Beware of the tigers!}, para.sub_macros(para.source).gsub(/>\s+<') end test 'should prepend value of imagesdir attribute to inline image target if target is relative path' do para = block_from_string %(Beware of the image:tiger.png[tiger].), :attributes => {'imagesdir' => './images'} assert_equal %{Beware of the tiger.}, para.sub_macros(para.source).gsub(/>\s+<') end test 'should not prepend value of imagesdir attribute to inline image target if target is absolute path' do para = block_from_string %(Beware of the image:/tiger.png[tiger].), :attributes => {'imagesdir' => './images'} assert_equal %{Beware of the tiger.}, para.sub_macros(para.source).gsub(/>\s+<') end test 'should not prepend value of imagesdir attribute to inline image target if target is url' do para = block_from_string %(Beware of the image:http://example.com/images/tiger.png[tiger].), :attributes => {'imagesdir' => './images'} assert_equal %{Beware of the tiger.}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a block image macro should not be detected within paragraph text' do para = block_from_string(%(Not an inline image macro image::tiger.png[].)) result = para.sub_macros(para.source) assert !result.include?(' {'icons' => ''} assert_equal %{github}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an icon macro should be interpreted as alt text if icons are disabled' do para = block_from_string 'icon:github[]' assert_equal %{[github]}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an icon macro should render alt text if icons are disabled and alt is given' do para = block_from_string 'icon:github[alt="GitHub"]' assert_equal %{[GitHub]}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an icon macro should be interpreted as a font-based icon when icons=font' do para = block_from_string 'icon:github[]', :attributes => {'icons' => 'font'} assert_equal %{}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an icon macro with a size should be interpreted as a font-based icon with a size when icons=font' do para = block_from_string 'icon:github[4x]', :attributes => {'icons' => 'font'} assert_equal %{}, para.sub_macros(para.source).gsub(/>\s+<') end test 'an icon macro with a role and title should be interpreted as a font-based icon with a class and title when icons=font' do para = block_from_string 'icon:heart[role="red", title="Heart me"]', :attributes => {'icons' => 'font'} assert_equal %{}, para.sub_macros(para.source).gsub(/>\s+<') end test 'a single-line footnote macro should be registered and rendered as a footnote' do para = block_from_string('Sentence text footnote:[An example footnote.].') assert_equal %(Sentence text [1].), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote = para.document.references[:footnotes].first assert_equal 1, footnote.index assert footnote.id.nil? assert_equal 'An example footnote.', footnote.text end test 'a multi-line footnote macro should be registered and rendered as a footnote' do para = block_from_string("Sentence text footnote:[An example footnote\nwith wrapped text.].") assert_equal %(Sentence text [1].), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote = para.document.references[:footnotes].first assert_equal 1, footnote.index assert footnote.id.nil? assert_equal "An example footnote\nwith wrapped text.", footnote.text end test 'a footnote macro can be directly adjacent to preceding word' do para = block_from_string('Sentence textfootnote:[An example footnote.].') assert_equal %(Sentence text[1].), para.sub_macros(para.source) end test 'a footnote macro may contain a macro' do para = block_from_string('Share your code. footnote:[http://github.com[GitHub]]') assert_equal %(Share your code. [1]), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote1 = para.document.references[:footnotes][0] assert_equal 'GitHub', footnote1.text end test 'a footnote macro may contain a plain URL' do para = block_from_string %(the JLine footnote:[https://github.com/jline/jline2]\nlibrary.) result = para.sub_macros para.source assert_equal %(the JLine [1]\nlibrary.), result assert_equal 1, para.document.references[:footnotes].size fn1 = para.document.references[:footnotes].first assert_equal 'https://github.com/jline/jline2', fn1.text end test 'a footnote macro followed by a semi-colon may contain a plain URL' do para = block_from_string %(the JLine footnote:[https://github.com/jline/jline2];\nlibrary.) result = para.sub_macros para.source assert_equal %(the JLine [1];\nlibrary.), result assert_equal 1, para.document.references[:footnotes].size fn1 = para.document.references[:footnotes].first assert_equal 'https://github.com/jline/jline2', fn1.text end test 'should increment index of subsequent footnote macros' do para = block_from_string("Sentence text footnote:[An example footnote.]. Sentence text footnote:[Another footnote.].") assert_equal %(Sentence text [1]. Sentence text [2].), para.sub_macros(para.source) assert_equal 2, para.document.references[:footnotes].size footnote1 = para.document.references[:footnotes][0] assert_equal 1, footnote1.index assert footnote1.id.nil? assert_equal "An example footnote.", footnote1.text footnote2 = para.document.references[:footnotes][1] assert_equal 2, footnote2.index assert footnote2.id.nil? assert_equal "Another footnote.", footnote2.text end test 'a footnoteref macro with id and single-line text should be registered and rendered as a footnote' do para = block_from_string('Sentence text footnoteref:[ex1, An example footnote.].') assert_equal %(Sentence text [1].), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote = para.document.references[:footnotes].first assert_equal 1, footnote.index assert_equal 'ex1', footnote.id assert_equal 'An example footnote.', footnote.text end test 'a footnoteref macro with id and multi-line text should be registered and rendered as a footnote' do para = block_from_string("Sentence text footnoteref:[ex1, An example footnote\nwith wrapped text.].") assert_equal %(Sentence text [1].), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote = para.document.references[:footnotes].first assert_equal 1, footnote.index assert_equal 'ex1', footnote.id assert_equal "An example footnote\nwith wrapped text.", footnote.text end test 'a footnoteref macro with id should refer to footnoteref with same id' do para = block_from_string('Sentence text footnoteref:[ex1, An example footnote.]. Sentence text footnoteref:[ex1].') assert_equal %(Sentence text [1]. Sentence text [1].), para.sub_macros(para.source) assert_equal 1, para.document.references[:footnotes].size footnote = para.document.references[:footnotes].first assert_equal 1, footnote.index assert_equal 'ex1', footnote.id assert_equal 'An example footnote.', footnote.text end test 'a single-line index term macro with a primary term should be registered as an index reference' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macros = ['indexterm:[Tigers]', '(((Tigers)))'] macros.each do |macro| para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Tigers'], para.document.references[:indexterms].first end end test 'a single-line index term macro with primary and secondary terms should be registered as an index reference' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macros = ['indexterm:[Big cats, Tigers]', '(((Big cats, Tigers)))'] macros.each do |macro| para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Big cats', 'Tigers'], para.document.references[:indexterms].first end end test 'a single-line index term macro with primary, secondary and tertiary terms should be registered as an index reference' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macros = ['indexterm:[Big cats,Tigers , Panthera tigris]', '(((Big cats,Tigers , Panthera tigris)))'] macros.each do |macro| para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Big cats', 'Tigers', 'Panthera tigris'], para.document.references[:indexterms].first end end test 'a multi-line index term macro should be compacted and registered as an index reference' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macros = ["indexterm:[Panthera\ntigris]", "(((Panthera\ntigris)))"] macros.each do |macro| para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Panthera tigris'], para.document.references[:indexterms].first end end test 'normal substitutions are performed on an index term macro' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macros = ['indexterm:[*Tigers*]', '(((*Tigers*)))'] macros.each do |macro| para = block_from_string("#{sentence}#{macro}") output = para.apply_normal_subs(para.lines) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Tigers'], para.document.references[:indexterms].first end end test 'registers multiple index term macros' do sentence = "The tiger (Panthera tigris) is the largest cat species." macros = "(((Tigers)))\n(((Animals,Cats)))" para = block_from_string("#{sentence}\n#{macros}") output = para.sub_macros(para.source) assert_equal sentence, output.rstrip assert_equal 2, para.document.references[:indexterms].size assert_equal ['Tigers'], para.document.references[:indexterms][0] assert_equal ['Animals', 'Cats'], para.document.references[:indexterms][1] end test 'an index term macro with round bracket syntax may contain round brackets in term' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macro = '(((Tiger (Panthera tigris))))' para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Tiger (Panthera tigris)'], para.document.references[:indexterms].first end test 'an index term macro with square bracket syntax may contain square brackets in term' do sentence = "The tiger (Panthera tigris) is the largest cat species.\n" macro = 'indexterm:[Tiger [Panthera tigris\\]]' para = block_from_string("#{sentence}#{macro}") output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['Tiger [Panthera tigris]'], para.document.references[:indexterms].first end test 'a single-line index term 2 macro should be registered as an index reference and retain term inline' do sentence = 'The tiger (Panthera tigris) is the largest cat species.' macros = ['The indexterm2:[tiger] (Panthera tigris) is the largest cat species.', 'The ((tiger)) (Panthera tigris) is the largest cat species.'] macros.each do |macro| para = block_from_string(macro) output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['tiger'], para.document.references[:indexterms].first end end test 'a multi-line index term 2 macro should be compacted and registered as an index reference and retain term inline' do sentence = 'The panthera tigris is the largest cat species.' macros = ["The indexterm2:[ panthera\ntigris ] is the largest cat species.", "The (( panthera\ntigris )) is the largest cat species."] macros.each do |macro| para = block_from_string(macro) output = para.sub_macros(para.source) assert_equal sentence, output assert_equal 1, para.document.references[:indexterms].size assert_equal ['panthera tigris'], para.document.references[:indexterms].first end end test 'registers multiple index term 2 macros' do sentence = "The ((tiger)) (Panthera tigris) is the largest ((cat)) species." para = block_from_string(sentence) output = para.sub_macros(para.source) assert_equal 'The tiger (Panthera tigris) is the largest cat species.', output assert_equal 2, para.document.references[:indexterms].size assert_equal ['tiger'], para.document.references[:indexterms][0] assert_equal ['cat'], para.document.references[:indexterms][1] end test 'normal substitutions are performed on an index term 2 macro' do sentence = 'The ((*tiger*)) (Panthera tigris) is the largest cat species.' para = block_from_string sentence output = para.apply_normal_subs(para.lines) assert_equal 'The tiger (Panthera tigris) is the largest cat species.', output assert_equal 1, para.document.references[:indexterms].size assert_equal ['tiger'], para.document.references[:indexterms].first end test 'index term 2 macro with round bracket syntex should not interfer with index term macro with round bracket syntax' do sentence = "The ((panthera tigris)) is the largest cat species.\n(((Big cats,Tigers)))" para = block_from_string sentence output = para.sub_macros(para.source) assert_equal "The panthera tigris is the largest cat species.\n", output terms = para.document.references[:indexterms] assert_equal 2, terms.size assert_equal ['Big cats', 'Tigers'], terms[0] assert_equal ['panthera tigris'], terms[1] end context 'Button macro' do test 'btn macro' do para = block_from_string('btn:[Save]', :attributes => {'experimental' => ''}) assert_equal %q{Save}, para.sub_macros(para.source) end test 'btn macro for docbook backend' do para = block_from_string('btn:[Save]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{Save}, para.sub_macros(para.source) end end context 'Keyboard macro' do test 'kbd macro with single key' do para = block_from_string('kbd:[F3]', :attributes => {'experimental' => ''}) assert_equal %q{F3}, para.sub_macros(para.source) end test 'kbd macro with single key, docbook backend' do para = block_from_string('kbd:[F3]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{F3}, para.sub_macros(para.source) end test 'kbd macro with key combination' do para = block_from_string('kbd:[Ctrl+Shift+T]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl+Shift+T}, para.sub_macros(para.source) end test 'kbd macro with key combination with spaces' do para = block_from_string('kbd:[Ctrl + Shift + T]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl+Shift+T}, para.sub_macros(para.source) end test 'kbd macro with key combination delimited by commas' do para = block_from_string('kbd:[Ctrl,Shift,T]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl+Shift+T}, para.sub_macros(para.source) end test 'kbd macro with key combination containing a plus key no spaces' do para = block_from_string('kbd:[Ctrl++]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl++}, para.sub_macros(para.source) end test 'kbd macro with key combination delimited by commands containing a comma key' do para = block_from_string('kbd:[Ctrl,,]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl+,}, para.sub_macros(para.source) end test 'kbd macro with key combination containing a plus key with spaces' do para = block_from_string('kbd:[Ctrl + +]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl++}, para.sub_macros(para.source) end test 'kbd macro with key combination containing escaped bracket' do para = block_from_string('kbd:[Ctrl + \]]', :attributes => {'experimental' => ''}) assert_equal %q{Ctrl+]}, para.sub_macros(para.source) end test 'kbd macro with key combination, docbook backend' do para = block_from_string('kbd:[Ctrl+Shift+T]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{CtrlShiftT}, para.sub_macros(para.source) end end context 'Menu macro' do test 'should process menu using macro sytnax' do para = block_from_string('menu:File[]', :attributes => {'experimental' => ''}) assert_equal %q{File}, para.sub_macros(para.source) end test 'should process menu for docbook backend' do para = block_from_string('menu:File[]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{File}, para.sub_macros(para.source) end test 'should process menu with menu item using macro syntax' do para = block_from_string('menu:File[Save As…]', :attributes => {'experimental' => ''}) assert_equal %q{File ▸ Save As…}, para.sub_macros(para.source) end test 'should process menu with menu item for docbook backend' do para = block_from_string('menu:File[Save As…]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{File Save As…}, para.sub_macros(para.source) end test 'should process menu with menu item in submenu using macro syntax' do para = block_from_string('menu:Tools[Project > Build]', :attributes => {'experimental' => ''}) assert_equal %q{Tools ▸ Project ▸ Build}, para.sub_macros(para.source) end test 'should process menu with menu item in submenu for docbook backend' do para = block_from_string('menu:Tools[Project > Build]', :backend => 'docbook', :attributes => {'experimental' => ''}) assert_equal %q{Tools Project Build}, para.sub_macros(para.source) end test 'should process menu with menu item in submenu using macro syntax and comma delimiter' do para = block_from_string('menu:Tools[Project, Build]', :attributes => {'experimental' => ''}) assert_equal %q{Tools ▸ Project ▸ Build}, para.sub_macros(para.source) end test 'should process menu with menu item using inline syntax' do para = block_from_string('"File > Save As…"', :attributes => {'experimental' => ''}) assert_equal %q{File ▸ Save As…}, para.sub_macros(para.source) end test 'should process menu with menu item in submenu using inline syntax' do para = block_from_string('"Tools > Project > Build"', :attributes => {'experimental' => ''}) assert_equal %q{Tools ▸ Project ▸ Build}, para.sub_macros(para.source) end test 'inline syntax should not closing quote of XML attribute' do para = block_from_string('<node>r', :attributes => {'experimental' => ''}) assert_equal %q{<node>r}, para.sub_macros(para.source) end end end context 'Passthroughs' do test 'collect inline triple plus passthroughs' do para = block_from_string('+++inline code+++') result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal 'inline code', para.passthroughs.first[:text] assert para.passthroughs.first[:subs].empty? end test 'collect multi-line inline triple plus passthroughs' do para = block_from_string("+++inline\ncode+++") result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal "inline\ncode", para.passthroughs.first[:text] assert para.passthroughs.first[:subs].empty? end test 'collect inline double dollar passthroughs' do para = block_from_string('$${code}$$') result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal '{code}', para.passthroughs.first[:text] assert_equal [:specialcharacters], para.passthroughs.first[:subs] end test 'collect multi-line inline double dollar passthroughs' do para = block_from_string("$$\n{code}\n$$") result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal "\n{code}\n", para.passthroughs.first[:text] assert_equal [:specialcharacters], para.passthroughs.first[:subs] end test 'collect passthroughs from inline pass macro' do para = block_from_string(%Q{pass:specialcharacters,quotes[['code'\\]]}) result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal %q{['code']}, para.passthroughs.first[:text] assert_equal [:specialcharacters, :quotes], para.passthroughs.first[:subs] end test 'collect multi-line passthroughs from inline pass macro' do para = block_from_string(%Q{pass:specialcharacters,quotes[['more\ncode'\\]]}) result = para.extract_passthroughs(para.source) assert_equal "\e" + '0' + "\e", result assert_equal 1, para.passthroughs.size assert_equal %Q{['more\ncode']}, para.passthroughs.first[:text] assert_equal [:specialcharacters, :quotes], para.passthroughs.first[:subs] end # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario) test 'restore inline passthroughs without subs' do para = block_from_string("some \e" + '0' + "\e to study") para.passthroughs << {:text => 'inline code', :subs => []} result = para.restore_passthroughs(para.source) assert_equal "some inline code to study", result end # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario) test 'restore inline passthroughs with subs' do para = block_from_string("some \e" + '0' + "\e to study in the \e" + '1' + "\e programming language") para.passthroughs << {:text => '{code}', :subs => [:specialcharacters]} para.passthroughs << {:text => '{language}', :subs => [:specialcharacters]} result = para.restore_passthroughs(para.source) assert_equal 'some <code>{code}</code> to study in the {language} programming language', result end test 'complex inline passthrough macro' do text_to_escape = %q{[(] <'basic form'> <'logical operator'> <'basic form'> [)]} para = block_from_string %($$#{text_to_escape}$$) result = para.extract_passthroughs(para.source) assert_equal 1, para.passthroughs.size assert_equal text_to_escape, para.passthroughs[0][:text] text_to_escape_escaped = %q{[(\] <'basic form'> <'logical operator'> <'basic form'> [)\]} para = block_from_string %(pass:specialcharacters[#{text_to_escape_escaped}]) result = para.extract_passthroughs(para.source) assert_equal 1, para.passthroughs.size assert_equal text_to_escape, para.passthroughs[0][:text] end test 'inline pass macro with a composite sub' do para = block_from_string %(pass:verbatim[<{backend}>]) assert_equal '<{backend}>', para.content end end context 'Replacements' do test 'unescapes XML entities' do para = block_from_string '< " " " >' assert_equal '< " " " >', para.apply_normal_subs(para.lines) end test 'replaces arrows' do para = block_from_string '<- -> <= => \<- \-> \<= \=>' assert_equal '← → ⇐ ⇒ <- -> <= =>', para.apply_normal_subs(para.source) end test 'replaces dashes' do para = block_from_string %(-- foo foo--bar foo\\--bar foo -- bar foo \\-- bar stuff in between -- foo stuff in between foo -- stuff in between foo --) expected = %( — foo foo—bar foo--bar foo — bar foo -- bar stuff in between — foo stuff in between foo — stuff in between foo — ) assert_equal expected, para.sub_replacements(para.source) end test 'replaces marks' do para = block_from_string '(C) (R) (TM) \(C) \(R) \(TM)' assert_equal '© ® ™ (C) (R) (TM)', para.sub_replacements(para.source) end test 'replaces punctuation' do para = block_from_string %(John's Hideout... foo\\'bar) assert_equal "John’s Hideout… foo'bar", para.sub_replacements(para.source) end end context 'Post replacements' do test 'line break inserted after line with line break character' do para = block_from_string("First line +\nSecond line") result = para.apply_subs(para.lines, :post_replacements, true) assert_equal "First line
    \n", result.first end test 'line break inserted after line wrap with hardbreaks enabled' do para = block_from_string("First line\nSecond line", :attributes => {'hardbreaks' => ''}) result = para.apply_subs(para.lines, :post_replacements, true) assert_equal "First line
    \n", result.first end test 'line break character stripped from end of line with hardbreaks enabled' do para = block_from_string("First line +\nSecond line", :attributes => {'hardbreaks' => ''}) result = para.apply_subs(para.lines, :post_replacements, true) assert_equal "First line
    \n", result.first end test 'line break not inserted for single line with hardbreaks enabled' do para = block_from_string("First line", :attributes => {'hardbreaks' => ''}) result = para.apply_subs(para.lines, :post_replacements, true) assert_equal "First line", result.first end end context 'Resolve subs' do test 'should resolve subs for block' do block = Asciidoctor::Block.new(empty_document, :paragraph) block.attributes['subs'] = 'quotes,normal' block.lock_in_subs assert_equal [:quotes, :specialcharacters, :attributes, :replacements, :macros, :post_replacements], block.subs end test 'should resolve specialcharacters sub as highlight for source block when source highlighter is coderay' do doc = empty_document :attributes => {'source-highlighter' => 'coderay'} block = Asciidoctor::Block.new(doc, :listing, :content_model => :verbatim) block.style = 'source' block.attributes['subs'] = 'specialcharacters' block.attributes['language'] = 'ruby' block.lock_in_subs assert_equal [:highlight], block.subs end test 'should resolve specialcharacters sub as highlight for source block when source highlighter is pygments' do doc = empty_document :attributes => {'source-highlighter' => 'pygments'} block = Asciidoctor::Block.new(doc, :listing, :content_model => :verbatim) block.style = 'source' block.attributes['subs'] = 'specialcharacters' block.attributes['language'] = 'ruby' block.lock_in_subs assert_equal [:highlight], block.subs end test 'should not resolve specialcharacters sub as highlight for source block when source highlighter is not set' do doc = empty_document block = Asciidoctor::Block.new(doc, :listing, :content_model => :verbatim) block.style = 'source' block.attributes['subs'] = 'specialcharacters' block.attributes['language'] = 'ruby' block.lock_in_subs assert_equal [:specialcharacters], block.subs end end end asciidoctor-0.1.4/test/tables_test.rb000066400000000000000000000563441221220517700176470ustar00rootroot00000000000000require 'test_helper' context 'Tables' do context 'PSV' do test 'renders simple psv table' do input = <<-EOS |======= |A |B |C |a |b |c |1 |2 |3 |======= EOS cells = [%w(A B C), %w(a b c), %w(1 2 3)] output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table.tableblock.frame-all.grid-all[style*="width:100%"]', output, 1 assert_css 'table > colgroup > col[style*="width:33%"]', output, 3 assert_css 'table tr', output, 3 assert_css 'table > tbody > tr', output, 3 assert_css 'table td', output, 9 assert_css 'table > tbody > tr > td.tableblock.halign-left.valign-top > p.tableblock', output, 9 cells.each_with_index {|row, rowi| assert_css "table > tbody > tr:nth-child(#{rowi + 1}) > td", output, row.size assert_css "table > tbody > tr:nth-child(#{rowi + 1}) > td > p", output, row.size row.each_with_index {|cell, celli| assert_xpath "(//tr)[#{rowi + 1}]/td[#{celli + 1}]/p[text()='#{cell}']", output, 1 } } end test 'renders caption on simple psv table' do input = <<-EOS .Simple psv table |======= |A |B |C |a |b |c |1 |2 |3 |======= EOS output = render_embedded_string input assert_xpath '/table/caption[@class="title"][text()="Table 1. Simple psv table"]', output, 1 assert_xpath '/table/caption/following-sibling::colgroup', output, 1 end test 'only increments table counter for tables that have a title' do input = <<-EOS .First numbered table |======= |1 |2 |3 |======= |======= |4 |5 |6 |======= .Second numbered table |======= |7 |8 |9 |======= EOS output = render_embedded_string input assert_css 'table:root', output, 3 assert_xpath '(/table)[1]/caption', output, 1 assert_xpath '(/table)[1]/caption[text()="Table 1. First numbered table"]', output, 1 assert_xpath '(/table)[2]/caption', output, 0 assert_xpath '(/table)[3]/caption', output, 1 assert_xpath '(/table)[3]/caption[text()="Table 2. Second numbered table"]', output, 1 end test 'renders explicit caption on simple psv table' do input = <<-EOS [caption="All the Data. "] .Simple psv table |======= |A |B |C |a |b |c |1 |2 |3 |======= EOS output = render_embedded_string input assert_xpath '/table/caption[@class="title"][text()="All the Data. Simple psv table"]', output, 1 assert_xpath '/table/caption/following-sibling::colgroup', output, 1 end test 'ignores escaped separators' do input = <<-EOS |=== |A \\| here| a \\| there |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > tbody > tr', output, 1 assert_css 'table > tbody > tr > td', output, 2 assert_xpath '/table/tbody/tr/td[1]/p[text()="A | here"]', output, 1 assert_xpath '/table/tbody/tr/td[2]/p[text()="a | there"]', output, 1 end test 'should auto recover with warning if missing leading separator on first cell' do input = <<-EOS |=== A | here| a | there |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 4 assert_css 'table > tbody > tr', output, 1 assert_css 'table > tbody > tr > td', output, 4 assert_xpath '/table/tbody/tr/td[1]/p[text()="A"]', output, 1 assert_xpath '/table/tbody/tr/td[2]/p[text()="here"]', output, 1 assert_xpath '/table/tbody/tr/td[3]/p[text()="a"]', output, 1 assert_xpath '/table/tbody/tr/td[4]/p[text()="there"]', output, 1 end test 'performs normal substitutions on cell content' do input = <<-EOS :show_title: Cool new show |=== |{show_title} |Coming soon... |=== EOS output = render_embedded_string input assert_xpath '//tbody/tr/td[1]/p[text()="Cool new show"]', output, 1 assert_xpath %(//tbody/tr/td[2]/p[text()='Coming soon#{[8230].pack('U*')}']), output, 1 end test 'table and col width not assigned when autowidth option is specified' do input = <<-EOS [options="autowidth"] |======= |A |B |C |a |b |c |1 |2 |3 |======= EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table[style*="width"]', output, 0 assert_css 'table colgroup col', output, 3 assert_css 'table colgroup col[width]', output, 0 end test 'first row sets number of columns when not specified' do input = <<-EOS |==== |first |second |third |fourth |1 |2 |3 |4 |==== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 4 assert_css 'table > tbody > tr', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td', output, 4 assert_css 'table > tbody > tr:nth-child(2) > td', output, 4 end test 'colspec attribute sets number of columns' do input = <<-EOS [cols="3*"] |=== |A |B |C |a |b |c |1 |2 |3 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > tbody > tr', output, 3 end test 'table with explicit column count can have multiple rows on a single line' do input = <<-EOS [cols="3*"] |=== |one |two |1 |2 |a |b |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 2 end test 'table with explicit deprecated syntax column count can have multiple rows on a single line' do input = <<-EOS [cols="3"] |=== |one |two |1 |2 |a |b |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 2 end test 'table with header and footer' do input = <<-EOS [frame="topbot",options="header,footer"] |=== |Item |Quantity |Item 1 |1 |Item 2 |2 |Item 3 |3 |Total |6 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > thead', output, 1 assert_css 'table > thead > tr', output, 1 assert_css 'table > thead > tr > th', output, 2 assert_css 'table > tfoot', output, 1 assert_css 'table > tfoot > tr', output, 1 assert_css 'table > tfoot > tr > td', output, 2 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 3 end test 'table with implicit header row' do input = <<-EOS |=== |Column 1 |Column 2 |Data A1 |Data B1 |Data A2 |Data B2 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > thead', output, 1 assert_css 'table > thead > tr', output, 1 assert_css 'table > thead > tr > th', output, 2 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 2 end test 'no implicit header row if second line not blank' do input = <<-EOS |=== |Column 1 |Column 2 |Data A1 |Data B1 |Data A2 |Data B2 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > thead', output, 0 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 3 end test 'no implicit header row if first line blank' do input = <<-EOS |=== |Column 1 |Column 2 |Data A1 |Data B1 |Data A2 |Data B2 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > thead', output, 0 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 3 end test 'no implicit header row if options is specified' do input = <<-EOS [options=""] |=== |Column 1 |Column 2 |Data A1 |Data B1 |Data A2 |Data B2 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 2 assert_css 'table > thead', output, 0 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 3 end test 'styles not applied to header cells' do input = <<-EOS [cols="1h,1s,1e",options="header,footer"] |==== |Name |Occupation| Website |Octocat |Social coding| http://github.com |Name |Occupation| Website |==== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > thead > tr > th', output, 3 assert_css 'table > thead > tr > th > *', output, 0 assert_css 'table > tfoot > tr > td', output, 3 assert_css 'table > tfoot > tr > td > p.header', output, 1 assert_css 'table > tfoot > tr > td > p > strong', output, 1 assert_css 'table > tfoot > tr > td > p > em', output, 1 assert_css 'table > tbody > tr > td', output, 3 assert_css 'table > tbody > tr > td > p.header', output, 1 assert_css 'table > tbody > tr > td > p > strong', output, 1 assert_css 'table > tbody > tr > td > p > em > a', output, 1 end test 'supports horizontal and vertical source data with blank lines and table header' do input = <<-EOS .Horizontal and vertical source data [width="80%",cols="3,^2,^2,10",options="header"] |=== |Date |Duration |Avg HR |Notes |22-Aug-08 |10:24 | 157 | Worked out MSHR (max sustainable heart rate) by going hard for this interval. |22-Aug-08 |23:03 | 152 | Back-to-back with previous interval. |24-Aug-08 |40:00 | 145 | Moderately hard interspersed with 3x 3min intervals (2 min hard + 1 min really hard taking the HR up to 160). I am getting in shape! |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table[style*="width:80%"]', output, 1 assert_xpath '/table/caption[@class="title"][text()="Table 1. Horizontal and vertical source data"]', output, 1 assert_css 'table > colgroup > col', output, 4 assert_css 'table > colgroup > col:nth-child(1)[@style*="width:17%"]', output, 1 assert_css 'table > colgroup > col:nth-child(2)[@style*="width:11%"]', output, 1 assert_css 'table > colgroup > col:nth-child(3)[@style*="width:11%"]', output, 1 assert_css 'table > colgroup > col:nth-child(4)[@style*="width:58%"]', output, 1 assert_css 'table > thead', output, 1 assert_css 'table > thead > tr', output, 1 assert_css 'table > thead > tr > th', output, 4 assert_css 'table > tbody > tr', output, 3 assert_css 'table > tbody > tr:nth-child(1) > td', output, 4 assert_css 'table > tbody > tr:nth-child(2) > td', output, 4 assert_css 'table > tbody > tr:nth-child(3) > td', output, 4 assert_xpath "/table/tbody/tr[1]/td[4]/p[text()='Worked out MSHR (max sustainable heart rate) by going hard\nfor this interval.']", output, 1 assert_css 'table > tbody > tr:nth-child(3) > td:nth-child(4) > p', output, 2 assert_xpath '/table/tbody/tr[3]/td[4]/p[2][text()="I am getting in shape!"]', output, 1 end test 'percentages as column widths' do input = <<-EOS [width="100%", cols="<.^10%,<90%"] |=== |column A |column B |=== EOS output = render_embedded_string input assert_xpath '/table/colgroup/col', output, 2 assert_xpath '(/table/colgroup/col)[1][@style="width:10%;"]', output, 1 assert_xpath '(/table/colgroup/col)[2][@style="width:90%;"]', output, 1 end test 'spans, alignments and styles' do input = <<-EOS [cols="e,m,^,>s",width="25%"] |=== |1 >s|2 |3 |4 ^|5 2.2+^.^|6 .3+<.>m|7 ^|8 d|9 2+>|10 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col[style*="width:25%"]', output, 4 assert_css 'table > tbody > tr', output, 4 assert_css 'table > tbody > tr > td', output, 10 assert_css 'table > tbody > tr:nth-child(1) > td', output, 4 assert_css 'table > tbody > tr:nth-child(2) > td', output, 3 assert_css 'table > tbody > tr:nth-child(3) > td', output, 1 assert_css 'table > tbody > tr:nth-child(4) > td', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(1).halign-left.valign-top p em', output, 1 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(2).halign-right.valign-top p strong', output, 1 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p', output, 1 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(3).halign-center.valign-top p *', output, 0 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(4).halign-right.valign-top p strong', output, 1 assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(1).halign-center.valign-top p em', output, 1 assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(2).halign-center.valign-middle[colspan="2"][rowspan="2"] p code', output, 1 assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(3).halign-left.valign-bottom[rowspan="3"] p code', output, 1 assert_css 'table > tbody > tr:nth-child(3) > td:nth-child(1).halign-center.valign-top p em', output, 1 assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p', output, 1 assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(1).halign-left.valign-top p em', output, 0 assert_css 'table > tbody > tr:nth-child(4) > td:nth-child(2).halign-right.valign-top[colspan="2"] p code', output, 1 end test 'supports repeating cells' do input = <<-EOS |=== 3*|A |1 3*|2 |b |c |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 3 assert_css 'table > tbody > tr:nth-child(1) > td', output, 3 assert_css 'table > tbody > tr:nth-child(2) > td', output, 3 assert_css 'table > tbody > tr:nth-child(3) > td', output, 3 assert_xpath '/table/tbody/tr[1]/td[1]/p[text()="A"]', output, 1 assert_xpath '/table/tbody/tr[1]/td[2]/p[text()="A"]', output, 1 assert_xpath '/table/tbody/tr[1]/td[3]/p[text()="A"]', output, 1 assert_xpath '/table/tbody/tr[2]/td[1]/p[text()="1"]', output, 1 assert_xpath '/table/tbody/tr[2]/td[2]/p[text()="2"]', output, 1 assert_xpath '/table/tbody/tr[2]/td[3]/p[text()="2"]', output, 1 assert_xpath '/table/tbody/tr[3]/td[1]/p[text()="2"]', output, 1 assert_xpath '/table/tbody/tr[3]/td[2]/p[text()="b"]', output, 1 assert_xpath '/table/tbody/tr[3]/td[3]/p[text()="c"]', output, 1 end test 'paragraph, verse and literal content' do input = <<-EOS [cols=",^v,^l",options="header"] |=== |Paragraphs |Verse |Literal 3*|The discussion about what is good, what is beautiful, what is noble, what is pure, and what is true could always go on. Why is that important? Why would I like to do that? Because that's the only conversation worth having. And whether it goes on or not after I die, I don't know. But, I do know that it is the conversation I want to have while I am still alive. Which means that to me the offer of certainty, the offer of complete security, the offer of an impermeable faith that can't give way is an offer of something not worth having. I want to live my life taking the risk all the time that I don't know anything like enough yet... that I haven't understood enough... that I can't know enough... that I am always hungrily operating on the margins of a potentially great harvest of future knowledge and wisdom. I wouldn't have it any other way. |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > thead', output, 1 assert_css 'table > thead > tr', output, 1 assert_css 'table > thead > tr > th', output, 3 assert_css 'table > tbody', output, 1 assert_css 'table > tbody > tr', output, 1 assert_css 'table > tbody > tr > td', output, 3 assert_css 'table > tbody > tr > td:nth-child(1).halign-left.valign-top > p.tableblock', output, 7 assert_css 'table > tbody > tr > td:nth-child(2).halign-center.valign-top > div.verse', output, 1 verse = xmlnodes_at_css 'table > tbody > tr > td:nth-child(2).halign-center.valign-top > div.verse', output, 1 assert_equal 26, verse.text.lines.entries.size assert_css 'table > tbody > tr > td:nth-child(3).halign-center.valign-top > div.literal > pre', output, 1 literal = xmlnodes_at_css 'table > tbody > tr > td:nth-child(3).halign-center.valign-top > div.literal > pre', output, 1 assert_equal 26, literal.text.lines.entries.size end test 'asciidoc content' do input = <<-EOS [cols="1e,1,5a",frame="topbot",options="header"] |=== |Name |Backends |Description |badges |xhtml11, html5 | Link badges ('XHTML 1.1' and 'CSS') in document footers. NOTE: The path names of images, icons and scripts are relative path names to the output document not the source document. |[[X97]] docinfo, docinfo1, docinfo2 |All backends | These three attributes control which document information files will be included in the the header of the output file: docinfo:: Include `-docinfo.` docinfo1:: Include `docinfo.` docinfo2:: Include `docinfo.` and `-docinfo.` Where `` is the file name (sans extension) of the AsciiDoc input file and `` is `.html` for HTML outputs or `.xml` for DocBook outputs. If the input file is the standard input then the output file name is used. |=== EOS doc = document_from_string input table = doc.blocks.first assert !table.nil? tbody = table.rows.body assert_equal 2, tbody.size body_cell_1_3 = tbody[0][2] assert !body_cell_1_3.inner_document.nil? assert body_cell_1_3.inner_document.nested? assert_equal doc, body_cell_1_3.inner_document.parent_document assert_equal doc.renderer, body_cell_1_3.inner_document.renderer output = doc.render assert_css 'table > tbody > tr', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td:nth-child(3) div.admonitionblock', output, 1 assert_css 'table > tbody > tr:nth-child(2) > td:nth-child(3) div.dlist', output, 1 end test 'preprocessor directive on first line of AsciiDoc cell should be processed' do input = <<-EOS |=== a|include::fixtures/include-file.asciidoc[] |=== EOS output = render_embedded_string input, :safe => :safe, :base_dir => File.dirname(__FILE__) assert_match(/included content/, output) end test 'nested table' do input = <<-EOS [cols="1,2a"] |=== |Normal cell |Cell with nested table [cols="2,1"] !=== !Nested table cell 1 !Nested table cell 2 !=== |=== EOS output = render_embedded_string input assert_css 'table', output, 2 assert_css 'table table', output, 1 assert_css 'table table', output, 1 assert_css 'table > tbody > tr > td:nth-child(2) table', output, 1 assert_css 'table > tbody > tr > td:nth-child(2) table > tbody > tr > td', output, 2 end test 'nested document in AsciiDoc cell should not see doctitle of parent' do input = <<-EOS = Document Title [cols="1a"] |=== |AsciiDoc content |=== EOS output = render_string input assert_css 'table', output, 1 assert_css 'table > tbody > tr > td', output, 1 assert_css 'table > tbody > tr > td #preamble', output, 0 assert_css 'table > tbody > tr > td .paragraph', output, 1 end test 'cell background color' do input = <<-EOS [cols="1e,1", options="header"] |=== |{set:cellbgcolor:green}green |{set:cellbgcolor!} plain |{set:cellbgcolor:red}red |{set:cellbgcolor!} plain |=== EOS output = render_embedded_string input assert_xpath '(/table/thead/tr/th)[1][@style="background-color:green;"]', output, 1 assert_xpath '(/table/thead/tr/th)[2][@style="background-color:green;"]', output, 0 assert_xpath '(/table/tbody/tr/td)[1][@style="background-color:red;"]', output, 1 assert_xpath '(/table/tbody/tr/td)[2][@style="background-color:green;"]', output, 0 end end context 'DSV' do test 'renders simple dsv table' do input = <<-EOS [width="75%",format="dsv"] |=== root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin mysql:x:27:27:MySQL\\:Server:/var/lib/mysql:/bin/bash gdm:x:42:42::/var/lib/gdm:/sbin/nologin sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin nobody:x:99:99:Nobody:/:/sbin/nologin |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col[style*="width:14%"]', output, 7 assert_css 'table > tbody > tr', output, 6 assert_xpath '//tr[4]/td[5]/p/text()', output, 0 assert_xpath '//tr[3]/td[5]/p[text()="MySQL:Server"]', output, 1 end test 'dsv format shorthand' do input = <<-EOS :=== a:b:c 1:2:3 :=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td', output, 3 assert_css 'table > tbody > tr:nth-child(2) > td', output, 3 end end context 'CSV' do test 'mixed unquoted records and quoted records with escaped quotes, commas and wrapped lines' do input = <<-EOS [format="csv",options="header"] |=== Year,Make,Model,Description,Price 1997,Ford,E350,"ac, abs, moon",3000.00 1999,Chevy,"Venture ""Extended Edition""","",4900.00 1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00 1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col[style*="width:20%"]', output, 5 assert_css 'table > thead > tr', output, 1 assert_css 'table > tbody > tr', output, 4 assert_xpath '((//tbody/tr)[1]/td)[4]/p[text()="ac, abs, moon"]', output, 1 assert_xpath %(((//tbody/tr)[2]/td)[3]/p[text()='Venture "Extended Edition"']), output, 1 assert_xpath '((//tbody/tr)[4]/td)[4]/p[text()="MUST SELL! air, moon roof, loaded"]', output, 1 end test 'csv format shorthand' do input = <<-EOS ,=== a,b,c 1,2,3 ,=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td', output, 3 assert_css 'table > tbody > tr:nth-child(2) > td', output, 3 end test 'custom separator' do input = <<-EOS [format="csv", separator=";"] |=== a;b;c 1;2;3 |=== EOS output = render_embedded_string input assert_css 'table', output, 1 assert_css 'table > colgroup > col', output, 3 assert_css 'table > tbody > tr', output, 2 assert_css 'table > tbody > tr:nth-child(1) > td', output, 3 assert_css 'table > tbody > tr:nth-child(2) > td', output, 3 end end end asciidoctor-0.1.4/test/test_helper.rb000066400000000000000000000211461221220517700176440ustar00rootroot00000000000000if RUBY_VERSION < '1.9' require 'rubygems' end require 'fileutils' require 'pathname' require 'test/unit' require "#{File.expand_path(File.dirname(__FILE__))}/../lib/asciidoctor.rb" require 'nokogiri' ENV['SUPPRESS_DEBUG'] ||= 'true' RE_XMLNS_ATTRIBUTE = / xmlns="[^"]+"/ class Test::Unit::TestCase def windows? RbConfig::CONFIG['host_os'] =~ /win|ming/ end def disk_root "#{windows? ? File.expand_path(__FILE__).split('/').first : nil}/" end def empty_document options = {} Asciidoctor::Document.new [], options end def empty_safe_document options = {} options[:safe] = :safe Asciidoctor::Document.new [], options end def sample_doc_path(name) name = name.to_s unless name.include?('.') ['asciidoc', 'txt'].each do |ext| if File.exist?(fixture_path("#{name}.#{ext}")) name = "#{name}.#{ext}" break end end end fixture_path(name) end def fixture_path(name) File.join(File.expand_path(File.dirname(__FILE__)), 'fixtures', name) end def example_document(name, opts = {}) document_from_string File.read(sample_doc_path(name)), opts end def assert_difference(expression, difference = 1, message = nil, &block) expressions = [expression] exps = expressions.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } before = exps.map { |e| e.call } yield expressions.zip(exps).each_with_index do |(code, e), i| error = "#{code.inspect} didn't change by #{difference}" error = "#{message}.\n#{error}" if message assert_equal(before[i] + difference, e.call, error) end end def xmlnodes_at_css(css, content, count = nil) xmlnodes_at_path(:css, css, content) end def xmlnodes_at_xpath(xpath, content, count = nil) xmlnodes_at_path(:xpath, xpath, content) end def xmlnodes_at_path(type, path, content, count = nil) doc = xmldoc_from_string content case type when :xpath namespaces = doc.respond_to?(:root) ? doc.root.namespaces : {} results = doc.xpath("#{path.sub('/', './')}", namespaces) when :css results = doc.css(path) end count == 1 ? results.first : results end # Generate an xpath attribute matcher that matches a name in the class attribute def contains_class(name) %(contains(concat(' ', normalize-space(@class), ' '), ' #{name} ')) end def assert_css(css, content, count = nil) assert_path(:css, css, content, count) end def assert_xpath(xpath, content, count = nil) assert_path(:xpath, xpath, content, count) end def assert_path(type, path, content, count = nil) case type when :xpath type_name = 'XPath' when :css type_name = 'CSS' end results = xmlnodes_at_path type, path, content if (count == true || count == false) if (count != results) flunk "#{type_name} #{path} yielded #{results} rather than #{count} for:\n#{content}" else assert true end elsif (count && results.length != count) flunk "#{type_name} #{path} yielded #{results.length} elements rather than #{count} for:\n#{content}" elsif (count.nil? && results.empty?) flunk "#{type_name} #{path} not found in:\n#{content}" else assert true end end def xmldoc_from_string(content) doctype_match = content.match(/\s* "<" # # Returns the String entity expanded to its equivalent UTF-8 glyph def expand_entity(number) [number].pack('U*') end alias :entity :expand_entity def invoke_cli_with_filenames(argv = [], filenames = [], &block) filepaths = Array.new filenames.each { |filename| if filenames.nil?|| ::Pathname.new(filename).absolute? filepaths.push(filename) else filepaths.push(File.join(File.dirname(__FILE__), 'fixtures', filename)) end } invoker = Asciidoctor::Cli::Invoker.new(argv + filepaths) invoker.invoke!(&block) invoker end def invoke_cli_to_buffer(argv = [], filename = 'sample.asciidoc', &block) invoke_cli(argv, filename, [StringIO.new, StringIO.new], &block) end def invoke_cli(argv = [], filename = 'sample.asciidoc', buffers = nil, &block) if filename.nil? || filename == '-' || ::Pathname.new(filename).absolute? filepath = filename else filepath = File.join(File.dirname(__FILE__), 'fixtures', filename) end invoker = Asciidoctor::Cli::Invoker.new(argv + [filepath]) if buffers invoker.redirect_streams(*buffers) end invoker.invoke!(&block) invoker end def redirect_streams old_stdout = $stdout old_stderr = $stderr stdout = StringIO.new stderr = StringIO.new $stdout = stdout $stderr = stderr begin yield(stdout, stderr) ensure $stdout = old_stdout $stderr = old_stderr end end end ### # # Context goodness provided by @citrusbyte's contest # ### # Test::Unit loads a default test if the suite is empty, whose purpose is to # fail. Since having empty contexts is a common practice, we decided to # overwrite TestSuite#empty? in order to allow them. Having a failure when no # tests have been defined seems counter-intuitive. class Test::Unit::TestSuite def empty? false end end # Contest adds +teardown+, +test+ and +context+ as class methods, and the # instance methods +setup+ and +teardown+ now iterate on the corresponding # blocks. Note that all setup and teardown blocks must be defined with the # block syntax. Adding setup or teardown instance methods defeats the purpose # of this library. class Test::Unit::TestCase def self.setup(&block) define_method :setup do super(&block) instance_eval(&block) end end def self.teardown(&block) define_method :teardown do instance_eval(&block) super(&block) end end def self.context(*name, &block) subclass = Class.new(self) remove_tests(subclass) subclass.class_eval(&block) if block_given? const_set(context_name(name.join(" ")), subclass) end def self.test(name, &block) define_method(test_name(name), &block) end class << self alias_method :should, :test alias_method :describe, :context end private def self.context_name(name) "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym end def self.test_name(name) "test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym end def self.sanitize_name(name) name.gsub(/\W+/, ' ').strip end def self.remove_tests(subclass) subclass.public_instance_methods.grep(/^test_/).each do |meth| subclass.send(:undef_method, meth.to_sym) end end end def context(*name, &block) Test::Unit::TestCase.context(name, &block) end asciidoctor-0.1.4/test/text_test.rb000066400000000000000000000153671221220517700173610ustar00rootroot00000000000000# encoding: UTF-8 require 'test_helper' context "Text" do test "proper encoding to handle utf8 characters in document using html backend" do output = example_document(:encoding).render assert_xpath '//p', output, 4 assert_xpath '//a', output, 1 end test "proper encoding to handle utf8 characters in embedded document using html backend" do output = example_document(:encoding, :header_footer => false).render assert_xpath '//p', output, 4 assert_xpath '//a', output, 1 end test "proper encoding to handle utf8 characters in document using docbook backend" do output = example_document(:encoding, :attributes => {'backend' => 'docbook'}).render assert_xpath '//xmlns:simpara', output, 4 assert_xpath '//xmlns:ulink', output, 1 end test "proper encoding to handle utf8 characters in embedded document using docbook backend" do output = example_document(:encoding, :header_footer => false, :attributes => {'backend' => 'docbook'}).render assert_xpath '//simpara', output, 4 assert_xpath '//ulink', output, 1 end # NOTE this test ensures we have the encoding line on block templates too test 'proper encoding to handle utf8 characters in arbitrary block' do input = [] input << "[verse]\n" input.concat(File.readlines(sample_doc_path(:encoding))) doc = empty_document reader = Asciidoctor::PreprocessorReader.new doc, input block = Asciidoctor::Lexer.next_block(reader, doc) assert_xpath '//pre', block.render.gsub(/^\s*\n/, ''), 1 end test 'proper encoding to handle utf8 characters from included file' do input = <<-EOS include::fixtures/encoding.asciidoc[tags=romé] EOS doc = empty_safe_document :base_dir => File.expand_path(File.dirname(__FILE__)) reader = Asciidoctor::PreprocessorReader.new doc, input block = Asciidoctor::Lexer.next_block(reader, doc) output = block.render assert_css '.paragraph', output, 1 end test 'escaped text markup' do assert_match(/All your <em>inline<\/em> markup belongs to <strong>us<\/strong>!/, render_string('All your inline markup belongs to us!')) end test "line breaks" do assert_xpath "//br", render_string("Well this is +\njust fine and dandy, isn't it?"), 1 end test "single- and double-quoted text" do rendered = render_string("``Where?,'' she said, flipping through her copy of `The New Yorker.'") assert_match(/“Where\?,”/, rendered) assert_match(/‘The New Yorker.’/, rendered) end test 'horizontal rule' do input = <<-EOS This line is separated by a horizontal rule... ''' ...from this line. EOS output = render_embedded_string input assert_xpath "//hr", output, 1 assert_xpath "/*[@class='paragraph']", output, 2 assert_xpath "(/*[@class='paragraph'])[1]/following-sibling::hr", output, 1 assert_xpath "/hr/following-sibling::*[@class='paragraph']", output, 1 end test 'markdown horizontal rules' do variants = [ '---', '- - -', '***', '* * *', '___', '_ _ _' ] offsets = [ '', ' ', ' ', ' ' ] variants.each do |variant| offsets.each do |offset| input = <<-EOS This line is separated by a horizontal rule... #{offset}#{variant} ...from this line. EOS output = render_embedded_string input assert_xpath "//hr", output, 1 assert_xpath "/*[@class='paragraph']", output, 2 assert_xpath "(/*[@class='paragraph'])[1]/following-sibling::hr", output, 1 assert_xpath "/hr/following-sibling::*[@class='paragraph']", output, 1 end end end test 'markdown horizontal rules negative case' do bad_variants = [ '- - - -', '* * * *', '_ _ _ _' ] good_offsets = [ '', ' ', ' ', ' ' ] bad_variants.each do |variant| good_offsets.each do |offset| input = <<-EOS This line is separated something that is not a horizontal rule... #{offset}#{variant} ...from this line. EOS output = render_embedded_string input assert_xpath '//hr', output, 0 end end good_variants = [ '- - -', '* * *', '_ _ _' ] bad_offsets = [ "\t", ' ' ] good_variants.each do |variant| bad_offsets.each do |offset| input = <<-EOS This line is separated something that is not a horizontal rule... #{offset}#{variant} ...from this line. EOS output = render_embedded_string input assert_xpath '//hr', output, 0 end end end test "emphasized text" do assert_xpath "//em", render_string("An 'emphatic' no") end test "emphasized text with single quote" do assert_xpath "//em[text()=\"Johnny#{[8217].pack('U*')}s\"]", render_string("It's 'Johnny's' phone") end test "emphasized text with escaped single quote" do assert_xpath "//em[text()=\"Johnny's\"]", render_string("It's 'Johnny\\'s' phone") end test "escaped single quote is restored as single quote" do assert_xpath "//p[contains(text(), \"Let's do it!\")]", render_string("Let\\'s do it!") end test "emphasized text at end of line" do assert_xpath "//em", render_string("This library is 'awesome'") end test "emphasized text at beginning of line" do assert_xpath "//em", render_string("'drop' it") end test "emphasized text across line" do assert_xpath "//em", render_string("'check it'") end test "unquoted text" do assert_no_match(/#/, render_string("An #unquoted# word")) end test "backtick-escaped text followed by single-quoted text" do assert_match(/foo<\/code>/, render_string(%Q(run `foo` 'dog'))) end context "basic styling" do setup do @rendered = render_string("A *BOLD* word. An _italic_ word. A +mono+ word. ^superscript!^ and some ~subscript~.") end test "strong" do assert_xpath "//strong", @rendered end test "italic" do assert_xpath "//em", @rendered end test "monospaced" do assert_xpath "//code", @rendered end test "superscript" do assert_xpath "//sup", @rendered end test "subscript" do assert_xpath "//sub", @rendered end test "backticks" do assert_xpath "//code", render_string("This is `totally cool`.") end test "nested styles" do rendered = render_string("Winning *big _time_* in the +city *boyeeee*+.") assert_xpath "//strong/em", rendered assert_xpath "//code/strong", rendered end test "unconstrained quotes" do rendered_chars = render_string("**B**__I__++M++") assert_xpath "//strong", rendered_chars assert_xpath "//em", rendered_chars assert_xpath "//code", rendered_chars end end end