diff options
author | Guillaume Grossetie <g.grossetie@gmail.com> | 2019-06-14 07:53:08 +0000 |
---|---|---|
committer | James Lopez <james@gitlab.com> | 2019-06-14 07:53:08 +0000 |
commit | 3f5d7c7e1c9a8b5ba53996e8a8f2f4881929b2ea (patch) | |
tree | 6b8c33c73a5391953e9da8c9d7b7f2c72295cf07 | |
parent | cd300323c8cc6744c46ef3f732c412226615abbf (diff) | |
download | gitlab-ce-3f5d7c7e1c9a8b5ba53996e8a8f2f4881929b2ea.tar.gz |
Add basic support for AsciiDoc include directive
See http://asciidoctor.org/docs/user-manual/#include-directive
-rw-r--r-- | Gemfile | 1 | ||||
-rw-r--r-- | Gemfile.lock | 3 | ||||
-rw-r--r-- | app/helpers/markup_helper.rb | 5 | ||||
-rw-r--r-- | changelogs/unreleased/asciidoc-include-directive.yml | 5 | ||||
-rw-r--r-- | doc/user/asciidoc.md | 372 | ||||
-rw-r--r-- | doc/user/project/repository/index.md | 2 | ||||
-rw-r--r-- | lib/gitlab/asciidoc.rb | 54 | ||||
-rw-r--r-- | lib/gitlab/asciidoc/html5_converter.rb | 32 | ||||
-rw-r--r-- | lib/gitlab/asciidoc/include_processor.rb | 126 | ||||
-rw-r--r-- | spec/lib/gitlab/asciidoc_spec.rb | 188 |
10 files changed, 747 insertions, 41 deletions
@@ -130,6 +130,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.8' +gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '0.0.8' gem 'rouge', '~> 3.1' gem 'truncato', '~> 0.7.11' diff --git a/Gemfile.lock b/Gemfile.lock index c403f45109c..0159d1f96e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,6 +67,8 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.8) + asciidoctor-include-ext (0.3.1) + asciidoctor (>= 1.5.6, < 3.0.0) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) ast (2.4.0) @@ -1024,6 +1026,7 @@ DEPENDENCIES apollo_upload_server (~> 2.0.0.beta3) asana (~> 0.8.1) asciidoctor (~> 1.5.8) + asciidoctor-include-ext (~> 0.3.1) asciidoctor-plantuml (= 0.0.8) attr_encrypted (~> 3.1.0) awesome_print diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index dce4168ad7b..bf894360a2e 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -263,6 +263,11 @@ module MarkupHelper end def asciidoc_unsafe(text, context = {}) + context.merge!( + commit: @commit, + ref: @ref, + requested_path: @path + ) Gitlab::Asciidoc.render(text, context) end diff --git a/changelogs/unreleased/asciidoc-include-directive.yml b/changelogs/unreleased/asciidoc-include-directive.yml new file mode 100644 index 00000000000..58fe3666727 --- /dev/null +++ b/changelogs/unreleased/asciidoc-include-directive.yml @@ -0,0 +1,5 @@ +--- +title: Add support for AsciiDoc include directive +merge_request: 28417 +author: "Jakub Jirutka & Guillaume Grossetie" +type: added diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md new file mode 100644 index 00000000000..a22b285b114 --- /dev/null +++ b/doc/user/asciidoc.md @@ -0,0 +1,372 @@ +# AsciiDoc + +GitLab uses the [Asciidoctor](https://asciidoctor.org) gem to convert AsciiDoc content to HTML5. +Consult the [Asciidoctor User Manual](https://asciidoctor.org/docs/user-manual) for a complete Asciidoctor reference. + +## Syntax + +Here's a brief reference of the most commonly used AsciiDoc syntax. +You can find the full documentation for the AsciiDoc syntax at https://asciidoctor.org/docs. + +### Paragraphs + +```asciidoc +A normal paragraph. +Line breaks are not preserved. +``` + +Line comments, which are lines that start with `//`, are skipped: + +``` +// this is a comment +``` + +A blank line separates paragraphs. + +A paragraph with the `[%hardbreaks]` option will preserve line breaks: + +```asciidoc +[%hardbreaks] +This paragraph carries the `hardbreaks` option. +Notice how line breaks are now preserved. +``` + +An indented (literal) paragraph disables text formatting, +preserves spaces and line breaks, and is displayed in a +monospaced font: + +```asciidoc + This literal paragraph is indented with one space. + As a consequence, *text formatting*, spaces, + and lines breaks will be preserved. +``` + +An admonition paragraph grabs the reader's attention: + +```asciidoc +NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs. + +TIP: Lists can be indented. Leading whitespace is not significant. +``` + +### Text Formatting + +**Constrained (applied at word boundaries)** + +```asciidoc +*strong importance* (aka bold) +_stress emphasis_ (aka italic) +`monospaced` (aka typewriter text) +"`double`" and '`single`' typographic quotes ++passthrough text+ (substitutions disabled) +`+literal text+` (monospaced with substitutions disabled) +``` + +**Unconstrained (applied anywhere)** + +```asciidoc +**C**reate+**R**ead+**U**pdate+**D**elete +fan__freakin__tastic +``mono``culture +``` + +**Replacements** + +```asciidoc +A long time ago in a galaxy far, far away... +(C) 1976 Arty Artisan +I believe I shall--no, actually I won't. +``` + +**Macros** + +```asciidoc +// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc. +The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow]. +The pass:c[->] operator is often referred to as the stabby lambda. +Since `pass:[++]` has strong priority in AsciiDoc, you can rewrite pass:c,a,r[C++ => C{pp}]. +// activate stem support by adding `:stem:` to the document header +stem:[sqrt(4) = 2] +``` + +### Attributes + +```asciidoc +// define attributes in the document header +:name: value +``` + +```asciidoc +:url-gem: https://rubygems.org/gems/asciidoctor + +You can download and install Asciidoctor {asciidoctor-version} from {url-gem}. +C{pp} is not required, only Ruby. +Use a leading backslash to output a word enclosed in curly braces, like \{name}. +``` + +### Links + +```asciidoc +https://example.org/page[A webpage] +link:../path/to/file.txt[A local file] +xref:document.adoc[A sibling document] +mailto:hello@example.org[Email to say hello!] +``` + +### Anchors + +```asciidoc +[[idname,reference text]] +// or written using normal block attributes as `[#idname,reftext=reference text]` +A paragraph (or any block) with an anchor (aka ID) and reftext. + +See <<idname>> or <<idname,optional text of internal link>>. + +xref:document.adoc#idname[Jumps to anchor in another document]. + +This paragraph has a footnote.footnote:[This is the text of the footnote.] +``` + +### Lists + +#### Unordered + +```asciidoc +* level 1 +** level 2 +*** level 3 +**** level 4 +***** etc. +* back at level 1 ++ +Attach a block or paragraph to a list item using a list continuation (which you can enclose in an open block). + +.Some Authors +[circle] +- Edgar Allen Poe +- Sheri S. Tepper +- Bill Bryson +``` + +#### Ordered + +```asciidoc +. Step 1 +. Step 2 +.. Step 2a +.. Step 2b +. Step 3 + +.Remember your Roman numerals? +[upperroman] +. is one +. is two +. is three +``` + +#### Checklist + +```asciidoc +* [x] checked +* [ ] not checked +``` +#### Callout + +```asciidoc +// enable callout bubbles by adding `:icons: font` to the document header +[,ruby] +---- +puts 'Hello, World!' # <1> +---- +<1> Prints `Hello, World!` to the console. +``` + +#### Description + +```asciidoc +first term:: description of first term +second term:: +description of second term +``` +### Document Structure + +#### Header + +```asciidoc += Document Title +Author Name <author@example.org> +v1.0, 2019-01-01 +``` +#### Sections + +```asciidoc += Document Title (Level 0) +== Level 1 +=== Level 2 +==== Level 3 +===== Level 4 +====== Level 5 +== Back at Level 1 +``` + +#### Includes + +```asciidoc +include::basics.adoc[] + +// define -a allow-uri-read to allow content to be read from URI +include::https://example.org/installation.adoc[] +``` +### Blocks + +```asciidoc +-- +open - a general-purpose content wrapper; useful for enclosing content to attach to a list item +-- +``` + +```asciidoc +// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING +// enable admonition icons by setting `:icons: font` in the document header +[NOTE] +==== +admonition - a notice for the reader, ranging in severity from a tip to an alert +==== +``` + +```asciidoc +==== +example - a demonstration of the concept being documented +==== +``` + +```asciidoc +.Toggle Me +[%collapsible] +==== +collapsible - these details are revealed by clicking the title +==== +``` + +```asciidoc +**** +sidebar - auxiliary content that can be read independently of the main content +**** +``` + +```asciidoc +.... +literal - an exhibit that features program output +.... +``` + +```asciidoc +---- +listing - an exhibit that features program input, source code, or the contents of a file +---- +``` + +```asciidoc +[,language] +---- +source - a listing that is embellished with (colorized) syntax highlighting +---- +``` + +```asciidoc +\```language +fenced code - a shorthand syntax for the source block +\``` +``` + +```asciidoc +[,attribution,citetitle] +____ +quote - a quotation or excerpt; attribution with title of source are optional +____ +``` + +```asciidoc +[verse,attribution,citetitle] +____ +verse - a literary excerpt, often a poem; attribution with title of source are optional +____ +``` + +```asciidoc +++++ +pass - content passed directly to the output document; often raw HTML +++++ +``` + +```asciidoc +// activate stem support by adding `:stem:` to the document header +[stem] +++++ +x = y^2 +++++ +``` + +```asciidoc +//// +comment - content which is not included in the output document +//// +``` + +### Tables + +```asciidoc +.Table Attributes +[cols=>1h;2d,width=50%,frame=topbot] +|=== +| Attribute Name | Values + +| options +| header,footer,autowidth + +| cols +| colspec[;colspec;...] + +| grid +| all \| cols \| rows \| none + +| frame +| all \| sides \| topbot \| none + +| stripes +| all \| even \| odd \| none + +| width +| (0%..100%) + +| format +| psv {vbar} csv {vbar} dsv +|=== +``` + +### Multimedia + +```asciidoc +image::screenshot.png[block image,800,450] + +Press image:reload.svg[reload,16,opts=interactive] to reload the page. + +video::movie.mp4[width=640,start=60,end=140,options=autoplay] + +video::aHjpOzsQ9YI[youtube] + +video::300817511[vimeo] +``` + +### Breaks + +```asciidoc +// thematic break (aka horizontal rule) +--- +``` + +```asciidoc +// page break +<<< +``` + diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 6fccfd40987..165f4c15165 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -68,7 +68,7 @@ according to the markup language. | Plain text | `txt` | | [Markdown](../../markdown.md) | `mdown`, `mkd`, `mkdn`, `md`, `markdown` | | [reStructuredText](http://docutils.sourceforge.net/rst.html) | `rst` | -| [Asciidoc](https://asciidoctor.org/docs/what-is-asciidoc/) | `adoc`, `ad`, `asciidoc` | +| [AsciiDoc](../../asciidoc.md) | `adoc`, `ad`, `asciidoc` | | [Textile](https://txstyle.org/) | `textile` | | [rdoc](http://rdoc.sourceforge.net/doc/index.html) | `rdoc` | | [Orgmode](https://orgmode.org/) | `org` | diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index df8f0470063..7f8300a0c2f 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,27 +1,41 @@ # frozen_string_literal: true require 'asciidoctor' -require 'asciidoctor/converter/html5' -require "asciidoctor-plantuml" +require 'asciidoctor-plantuml' +require 'asciidoctor/extensions' +require 'gitlab/asciidoc/html5_converter' module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters # the resulting HTML through HTML pipeline filters. module Asciidoc - DEFAULT_ADOC_ATTRS = [ - 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', - 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font', - 'outfilesuffix=.adoc' - ].freeze + MAX_INCLUDE_DEPTH = 5 + DEFAULT_ADOC_ATTRS = { + 'showtitle' => true, + 'idprefix' => 'user-content-', + 'idseparator' => '-', + 'env' => 'gitlab', + 'env-gitlab' => '', + 'source-highlighter' => 'html-pipeline', + 'icons' => 'font', + 'outfilesuffix' => '.adoc', + 'max-include-depth' => MAX_INCLUDE_DEPTH + }.freeze # Public: Converts the provided Asciidoc markup into HTML. # # input - the source text in Asciidoc format + # context - :commit, :project, :ref, :requested_path # def self.render(input, context) + extensions = proc do + include_processor ::Gitlab::Asciidoc::IncludeProcessor.new(context) + end + asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: DEFAULT_ADOC_ATTRS } + attributes: DEFAULT_ADOC_ATTRS, + extensions: extensions } context[:pipeline] = :ascii_doc @@ -40,29 +54,5 @@ module Gitlab conf.txt_enable = false end end - - class Html5Converter < Asciidoctor::Converter::Html5Converter - extend Asciidoctor::Converter::Config - - register_for 'gitlab_html5' - - def stem(node) - return super unless node.style.to_sym == :latexmath - - %(<pre#{id_attribute(node)} data-math-style="display"><code>#{node.content}</code></pre>) - end - - def inline_quoted(node) - return super unless node.type.to_sym == :latexmath - - %(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>) - end - - private - - def id_attribute(node) - node.id ? %( id="#{node.id}") : nil - end - end end end diff --git a/lib/gitlab/asciidoc/html5_converter.rb b/lib/gitlab/asciidoc/html5_converter.rb new file mode 100644 index 00000000000..2c5c74e4789 --- /dev/null +++ b/lib/gitlab/asciidoc/html5_converter.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'asciidoctor' +require 'asciidoctor/converter/html5' + +module Gitlab + module Asciidoc + class Html5Converter < Asciidoctor::Converter::Html5Converter + extend Asciidoctor::Converter::Config + + register_for 'gitlab_html5' + + def stem(node) + return super unless node.style.to_sym == :latexmath + + %(<pre#{id_attribute(node)} data-math-style="display"><code>#{node.content}</code></pre>) + end + + def inline_quoted(node) + return super unless node.type.to_sym == :latexmath + + %(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>) + end + + private + + def id_attribute(node) + node.id ? %( id="#{node.id}") : nil + end + end + end +end diff --git a/lib/gitlab/asciidoc/include_processor.rb b/lib/gitlab/asciidoc/include_processor.rb new file mode 100644 index 00000000000..c6fbf540e9c --- /dev/null +++ b/lib/gitlab/asciidoc/include_processor.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'asciidoctor/include_ext/include_processor' + +module Gitlab + module Asciidoc + # Asciidoctor extension for processing includes (macro include::[]) within + # documents inside the same repository. + class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor + extend ::Gitlab::Utils::Override + + def initialize(context) + super(logger: Gitlab::AppLogger) + + @context = context + @repository = context[:project].try(:repository) + + # Note: Asciidoctor calls #freeze on extensions, so we can't set new + # instance variables after initialization. + @cache = { + uri_types: {} + } + end + + protected + + override :include_allowed? + def include_allowed?(target, reader) + doc = reader.document + + return false if doc.attributes.fetch('max-include-depth').to_i < 1 + return false if target_uri?(target) + + true + end + + override :resolve_target_path + def resolve_target_path(target, reader) + return unless repository.try(:exists?) + + base_path = reader.include_stack.empty? ? requested_path : reader.file + path = resolve_relative_path(target, base_path) + + path if Gitlab::Git::Blob.find(repository, ref, path) + end + + override :read_lines + def read_lines(filename, selector) + blob = read_blob(ref, filename) + + if selector + blob.data.each_line.select.with_index(1, &selector) + else + blob.data + end + end + + override :unresolved_include! + def unresolved_include!(target, reader) + reader.unshift_line("*[ERROR: include::#{target}[] - unresolved directive]*") + end + + private + + attr_accessor :context, :repository, :cache + + # Gets a Blob at a path for a specific revision. + # This method will check that the Blob exists and contains readable text. + # + # revision - The String SHA1. + # path - The String file path. + # + # Returns a Blob + def read_blob(ref, filename) + blob = repository&.blob_at(ref, filename) + + raise 'Blob not found' unless blob + raise 'File is not readable' unless blob.readable_text? + + blob + end + + # Resolves the given relative path of file in repository into canonical + # path based on the specified base_path. + # + # Examples: + # + # # File in the same directory as the current path + # resolve_relative_path("users.adoc", "doc/api/README.adoc") + # # => "doc/api/users.adoc" + # + # # File in the same directory, which is also the current path + # resolve_relative_path("users.adoc", "doc/api") + # # => "doc/api/users.adoc" + # + # # Going up one level to a different directory + # resolve_relative_path("../update/7.14-to-8.0.adoc", "doc/api/README.adoc") + # # => "doc/update/7.14-to-8.0.adoc" + # + # Returns a String + def resolve_relative_path(path, base_path) + p = Pathname(base_path) + p = p.dirname unless p.extname.empty? + p += path + + p.cleanpath.to_s + end + + def current_commit + cache[:current_commit] ||= context[:commit] || repository&.commit(ref) + end + + def ref + context[:ref] || context[:project].default_branch + end + + def requested_path + cache[:requested_path] ||= Addressable::URI.unescape(context[:requested_path]) + end + + def uri_type(path) + cache[:uri_types][path] ||= current_commit&.uri_type(path) + end + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index e1782cff81a..0f933ac5464 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -3,20 +3,23 @@ require 'nokogiri' module Gitlab describe Asciidoc do - let(:input) { '<b>ascii</b>' } - let(:context) { {} } - let(:html) { 'H<sub>2</sub>O' } + include FakeBlobHelpers + + before do + allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) + end context "without project" do - before do - allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) - end + let(:input) { '<b>ascii</b>' } + let(:context) { {} } + let(:html) { 'H<sub>2</sub>O' } it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS + attributes: described_class::DEFAULT_ADOC_ATTRS, + extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) @@ -30,7 +33,8 @@ module Gitlab expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS + attributes: described_class::DEFAULT_ADOC_ATTRS, + extensions: be_a(Proc) } expect(Asciidoctor).to receive(:convert) @@ -105,6 +109,174 @@ module Gitlab end end + context 'with project' do + let(:context) do + { + commit: commit, + project: project, + ref: ref, + requested_path: requested_path + } + end + let(:commit) { project.commit(ref) } + let(:project) { create(:project, :repository) } + let(:ref) { 'asciidoc' } + let(:requested_path) { '/' } + + context 'include directive' do + subject(:output) { render(input, context) } + + let(:input) { "Include this:\n\ninclude::#{include_path}[]" } + + before do + current_file = requested_path + current_file += 'README.adoc' if requested_path.end_with? '/' + + create_file(current_file, "= AsciiDoc\n") + end + + context 'with path to non-existing file' do + let(:include_path) { 'not-exists.adoc' } + + it 'renders Unresolved directive placeholder' do + is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>") + end + end + + shared_examples :invalid_include do + let(:include_path) { 'dk.png' } + + before do + allow(project.repository).to receive(:blob_at).and_return(blob) + end + + it 'does not read the blob' do + expect(blob).not_to receive(:data) + end + + it 'renders Unresolved directive placeholder' do + is_expected.to include("<strong>[ERROR: include::#{include_path}[] - unresolved directive]</strong>") + end + end + + context 'with path to a binary file' do + let(:blob) { fake_blob(path: 'dk.png', binary: true) } + include_examples :invalid_include + end + + context 'with path to file in external storage' do + let(:blob) { fake_blob(path: 'dk.png', lfs: true) } + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + end + + include_examples :invalid_include + end + + context 'with path to a textual file' do + let(:include_path) { 'sample.adoc' } + + before do + create_file(file_path, "Content from #{include_path}") + end + + shared_examples :valid_include do + [ + ['/doc/sample.adoc', 'doc/sample.adoc', 'absolute path'], + ['sample.adoc', 'doc/api/sample.adoc', 'relative path'], + ['./sample.adoc', 'doc/api/sample.adoc', 'relative path with leading ./'], + ['../sample.adoc', 'doc/sample.adoc', 'relative path to a file up one directory'], + ['../../sample.adoc', 'sample.adoc', 'relative path for a file up multiple directories'] + ].each do |include_path_, file_path_, desc| + + context "the file is specified by #{desc}" do + let(:include_path) { include_path_ } + let(:file_path) { file_path_ } + + it 'includes content of the file' do + is_expected.to include('<p>Include this:</p>') + is_expected.to include("<p>Content from #{include_path}</p>") + end + end + end + end + + context 'when requested path is a file in the repo' do + let(:requested_path) { 'doc/api/README.adoc' } + + include_examples :valid_include + + context 'without a commit (only ref)' do + let(:commit) { nil } + include_examples :valid_include + end + end + + context 'when requested path is a directory in the repo' do + let(:requested_path) { 'doc/api/' } + + include_examples :valid_include + + context 'without a commit (only ref)' do + let(:commit) { nil } + include_examples :valid_include + end + end + end + + context 'recursive includes with relative paths' do + let(:input) do + <<~ADOC + Source: requested file + + include::doc/README.adoc[] + + include::license.adoc[] + ADOC + end + + before do + create_file 'doc/README.adoc', <<~ADOC + Source: doc/README.adoc + + include::../license.adoc[] + + include::api/hello.adoc[] + ADOC + create_file 'license.adoc', <<~ADOC + Source: license.adoc + ADOC + create_file 'doc/api/hello.adoc', <<~ADOC + Source: doc/api/hello.adoc + + include::./common.adoc[] + ADOC + create_file 'doc/api/common.adoc', <<~ADOC + Source: doc/api/common.adoc + ADOC + end + + it 'includes content of the included files recursively' do + expect(output.gsub(/<[^>]+>/, '').gsub(/\n\s*/, "\n").strip).to eq <<~ADOC.strip + Source: requested file + Source: doc/README.adoc + Source: license.adoc + Source: doc/api/hello.adoc + Source: doc/api/common.adoc + Source: license.adoc + ADOC + end + end + + def create_file(path, content) + project.repository.create_file(project.creator, path, content, + message: "Add #{path}", branch_name: 'asciidoc') + end + end + end + def render(*args) described_class.render(*args) end |