diff options
author | blackst0ne <blackst0ne.ru@gmail.com> | 2018-05-18 10:25:59 +0000 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2018-05-18 10:25:59 +0000 |
commit | c0e77f7c9cc24104981bb8f6973ceeb9c311e1e2 (patch) | |
tree | 54402d82ae262b212613408c011477e664ace0d3 | |
parent | 9f863dbe1a5ed324b7fb202eea11397d1fcdd61f (diff) | |
download | gitlab-ce-c0e77f7c9cc24104981bb8f6973ceeb9c311e1e2.tar.gz |
Resolve "Expand API: Render an arbitrary Markdown document"
-rw-r--r-- | changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml | 5 | ||||
-rw-r--r-- | doc/api/README.md | 1 | ||||
-rw-r--r-- | doc/api/markdown.md | 29 | ||||
-rw-r--r-- | lib/api/api.rb | 1 | ||||
-rw-r--r-- | lib/api/markdown.rb | 33 | ||||
-rw-r--r-- | lib/banzai/filter/reference_filter.rb | 2 | ||||
-rw-r--r-- | lib/banzai/pipeline/gfm_pipeline.rb | 4 | ||||
-rw-r--r-- | spec/requests/api/markdown_spec.rb | 112 |
8 files changed, 184 insertions, 3 deletions
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml new file mode 100644 index 00000000000..a97e8a2b5cc --- /dev/null +++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to render markdown text +merge_request: 18926 +author: "@blackst0ne" +type: added diff --git a/doc/api/README.md b/doc/api/README.md index e777fc63d2b..194907accc7 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,6 +33,7 @@ following locations: - [Jobs](jobs.md) - [Keys](keys.md) - [Labels](labels.md) +- [Markdown](markdown.md) - [Merge Requests](merge_requests.md) - [Project milestones](milestones.md) - [Group milestones](group_milestones.md) diff --git a/doc/api/markdown.md b/doc/api/markdown.md new file mode 100644 index 00000000000..f406838e887 --- /dev/null +++ b/doc/api/markdown.md @@ -0,0 +1,29 @@ +# Markdown API + +> [Introduced][ce-18926] in GitLab 11.0. + +Available only in APIv4. + +## Render an arbitrary Markdown document + +``` +POST /api/v4/markdown +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | ------------- | ------------------------------------------ | +| `text` | string | yes | The markdown text to render | +| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` | +| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. | + +```bash +curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown +``` + +Response example: + +```json +{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" } +``` + +[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926 diff --git a/lib/api/api.rb b/lib/api/api.rb index 2fbeaaffcfe..de20b2b8e67 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -140,6 +140,7 @@ module API mount ::API::Keys mount ::API::Labels mount ::API::Lint + mount ::API::Markdown mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb new file mode 100644 index 00000000000..b9ed68aa584 --- /dev/null +++ b/lib/api/markdown.rb @@ -0,0 +1,33 @@ +module API + class Markdown < Grape::API + params do + requires :text, type: String, desc: "The markdown text to render" + optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown" + optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown" + end + resource :markdown do + desc "Render markdown text" do + detail "This feature was introduced in GitLab 11.0." + end + post do + # Explicitly set CommonMark as markdown engine to use. + # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done. + context = { markdown_engine: :common_mark, only_path: false } + + if params[:project] + project = Project.find_by_full_path(params[:project]) + + not_found!("Project") unless can?(current_user, :read_project, project) + + context[:project] = project + else + context[:skip_project_check] = true + end + + context[:pipeline] = params[:gfm] ? :full : :plain_markdown + + { html: Banzai.render(params[:text], context) } + end + end + end +end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index b9d5ecf70ec..2f023f4f242 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -73,7 +73,7 @@ module Banzai # # Note that while the key might exist, its value could be nil! def validate - needs :project + needs :project unless skip_project_check? end # Iterates over all <a> and text() nodes in a document. diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8b2f05fffec..a1f24e8b093 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -42,9 +42,9 @@ module Banzai end def self.transform_context(context) - context.merge( - only_path: true, + context[:only_path] = true unless context.key?(:only_path) + context.merge( # EmojiFilter asset_host: Gitlab::Application.config.asset_host, asset_root: Gitlab.config.gitlab.base_url diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb new file mode 100644 index 00000000000..a55796cf343 --- /dev/null +++ b/spec/requests/api/markdown_spec.rb @@ -0,0 +1,112 @@ +require "spec_helper" + +describe API::Markdown do + RSpec::Matchers.define_negated_matcher :exclude, :include + + describe "POST /markdown" do + let(:user) {} # No-op. It gets overwritten in the contexts below. + + before do + post api("/markdown", user), params + end + + shared_examples "rendered markdown text without GFM" do + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to eq("<p>#{text}</p>") + end + end + + shared_examples "404 Project Not Found" do + it "responses with 404 Not Found" do + expect(response).to have_http_status(404) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["message"]).to eq("404 Project Not Found") + end + end + + context "when arguments are invalid" do + context "when text is missing" do + let(:params) { {} } + + it "responses with 400 Bad Request" do + expect(response).to have_http_status(400) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["error"]).to eq("text is missing") + end + end + + context "when project is not found" do + let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } } + + it_behaves_like "404 Project Not Found" + end + end + + context "when arguments are valid" do + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" } + + context "when not using gfm" do + context "without project" do + let(:params) { { text: text } } + + it_behaves_like "rendered markdown text without GFM" + end + + context "with project" do + let(:params) { { text: text, project: project.full_path } } + + context "when not authorized" do + it_behaves_like "404 Project Not Found" + end + + context "when authorized" do + let(:user) { project.owner } + + it_behaves_like "rendered markdown text without GFM" + end + end + end + + context "when using gfm" do + context "without project" do + let(:params) { { text: text, gfm: true } } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("#1") + .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and exclude("#1</a>") + end + end + + context "with project" do + let(:params) { { text: text, gfm: true, project: project.full_path } } + let(:user) { project.owner } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and include("#1</a>") + end + end + end + end + end +end |