From e9e19a668bd666a384ac8b48cfd9b6006e674859 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Thu, 13 Jan 2022 08:45:15 +1100 Subject: [PATCH] feat: added markdown support --- examples/basic.yml | 6 +-- packages/tribble-app/Cargo.toml | 1 + packages/tribble-app/src/parser.rs | 21 ++++---- .../tribble-app/src/templates/workflow/mod.rs | 1 + .../src/templates/workflow/parse_md.rs | 14 ++++++ .../src/templates/workflow/view.rs | 12 +++-- packages/tribble-app/static/style.css | 50 +++++++++++++++++++ packages/tribble-app/tailwind.config.js | 4 +- 8 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 packages/tribble-app/src/templates/workflow/parse_md.rs diff --git a/examples/basic.yml b/examples/basic.yml index 4ca2e02..302b267 100644 --- a/examples/basic.yml +++ b/examples/basic.yml @@ -2,7 +2,7 @@ workflows: test: sections: Start: - - "Hello, and welcome to the first stage of your contribution journey with Tribble!" + - "Hello, and welcome to the first stage of your contribution journey with [Tribble](https://github.com/arctic-hen7/tribble)! `This is some code.`" - { text: "I want to report a bug", link: "Report Bug", tags: [ "C:bug" ] } - { text: "I want to suggest an enhancement", link: "Request Enhancement", tags: [ "C:enhancement" ] } - { text: "I'd like to add something to the docs", link: "endpoint:Documentation", tags: [ "C:docs" ] } @@ -21,7 +21,7 @@ workflows: index: Start endpoints: Bug: - preamble: "Thank you very much for reporting this bug, we'll get on it right away!" + preamble: "Thank you very much for reporting this bug, [we'll](https://example.com) get on it right away!" text: "This report is reporting a bug. Description: ${bug_description}. Boolean: ${bool}" dest_text: "Report on GitHub" dest_url: "#" @@ -30,4 +30,4 @@ workflows: text: "This report is requesting an enhancement to the ${feature_area}." dest_text: "Report on GitHub" dest_url: "#" - Documentation: "You can contribute to the docs by doing foobar!" + Documentation: "You can contribute to the docs by doing *foobar*!" diff --git a/packages/tribble-app/Cargo.toml b/packages/tribble-app/Cargo.toml index 3087f48..35a5e01 100644 --- a/packages/tribble-app/Cargo.toml +++ b/packages/tribble-app/Cargo.toml @@ -27,6 +27,7 @@ wasm-bindgen-futures = "0.4" futures = "0.3" perseus-size-opt = "0.1" schemars = { version = "0.8", optional = true } +pulldown-cmark = "0.8" [features] schema = [ "schemars" ] diff --git a/packages/tribble-app/src/parser.rs b/packages/tribble-app/src/parser.rs index 0b15a41..bc36246 100644 --- a/packages/tribble-app/src/parser.rs +++ b/packages/tribble-app/src/parser.rs @@ -10,6 +10,7 @@ fn default_input_err_msg() -> String { } /// The possible types of configuration files (this allows main files to be different from internationalization files). +// Note: Markdown is supported in three places: an instructional endpoint, the preamble of a report endpoint, and a text element in a section. #[cfg_attr(feature = "schema", derive(JsonSchema))] #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] @@ -21,7 +22,7 @@ pub enum Config { }, /// A configuration file for a single language. Language { - /// The error message when a user doesn't fill out a mandatory field. This is allowed to enable i18n at an arbitrary scale. + /// The error message when a user doesn't fill out a mandatory field. This is allowed to enable i18n at an arbitrary scale. This field does not support Markdown. #[serde(default = "default_input_err_msg")] input_err_msg: String, /// All the workflow in this Tribble instance. Each workflow is a separate contribution experience, and multiple workflows are generally best suited for things like separate products. @@ -65,12 +66,11 @@ pub type Section = Vec; #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(untagged)] pub enum SectionElem { - /// Simple text to be displayed to the user. If this begins with a `<` that's unescaped, it will be treated as arbitrary HTML, and will be directly injected into the page. In that - /// case, it is assumed to be sanitized. + /// Simple text to be displayed to the user. Markdown is supported here, and this will be rendered to HTML to be interpolated into the page. Text(String), /// A progression option for moving to another section. Progression { - /// The text to display to the user. + /// The text to display to the user. This does not support Markdown, as it goes inside an HTML `button`. text: String, /// The name of the section to navigate to. If this is prefixed with `endpoint:`, it will navigate to an endpoint instead of a section. link: String, @@ -86,7 +86,7 @@ pub enum SectionElem { pub struct InputSectionElem { /// The input's ID, which can be used to reference its value later for interpolation in a formatted report. pub id: String, - /// The label for the input. + /// The label for the input. This does not support Markdown. pub label: String, /// Whether or not the input is optional. #[serde(default)] @@ -209,7 +209,7 @@ pub enum SelectOption { /// A select element that simply has a value. Simple(String), WithTags { - /// The displayed text of the option. + /// The displayed text of the option. This does not support Markdown. text: String, /// A list of tags that should be accumulated if this option is selected. If multiple options can be selected and there are duplications, tags will only be assigned once. tags: Vec, @@ -223,16 +223,17 @@ pub enum Endpoint { /// A report endpoint, which gives the user a formatted report in Markdown to send to the project. // TODO Add functionality to actually send the report somewhere Report { - /// The preamble text to display before the actual formatted report. + /// The preamble text to display before the actual formatted report. Markdown can be used here. preamble: String, - /// The formatted report. The UI will not allow the user to edit this, but will provide a copy button. Interpolation of form values is allowed here with `${form_id}` syntax. + /// The formatted report. The UI will not allow the user to edit this, but will provide a copy button. Interpolation of form values is allowed here with `${form_id}` syntax. This + /// should be written in the appropriate templating language for your issue reporting system (e.g. Markdown for GitHub issues), and will be displayed as a raw, pre-formatted string. text: String, - /// The text of a button for sending teh user to wherever they'll report the issue. + /// The text of a button for sending teh user to wherever they'll report the issue. This does not support Markdown. dest_text: String, /// A URL to send the user to so that they can report the issue. If the platform supports interpolating text to be sent /// into the URL, you can do so by interpolating `%r` into this field. dest_url: String, }, - /// An instructional endpoint, which tells the user to do something. + /// An instructional endpoint, which tells the user to do something. This supports Markdown. Instructional(String), } diff --git a/packages/tribble-app/src/templates/workflow/mod.rs b/packages/tribble-app/src/templates/workflow/mod.rs index 34b88aa..cdd4828 100644 --- a/packages/tribble-app/src/templates/workflow/mod.rs +++ b/packages/tribble-app/src/templates/workflow/mod.rs @@ -1,5 +1,6 @@ mod get_build_paths; mod get_build_state; +mod parse_md; mod view; use perseus::{Html, Template}; diff --git a/packages/tribble-app/src/templates/workflow/parse_md.rs b/packages/tribble-app/src/templates/workflow/parse_md.rs new file mode 100644 index 0000000..b9061a1 --- /dev/null +++ b/packages/tribble-app/src/templates/workflow/parse_md.rs @@ -0,0 +1,14 @@ +use pulldown_cmark::{html, Options, Parser}; + +/// Parses the given Markdown into an HTML string. This does not perform any santizing of the resulting HTML, so only parse trusted content through here! (This is designed to be used on +/// user-given strings from their Tribble configurations, which they're then serving as their own websites, so security shouldn't be a problem here.) +pub fn parse_md_to_html(markdown: &str) -> String { + let mut opts = Options::empty(); + opts.insert(Options::ENABLE_STRIKETHROUGH); + opts.insert(Options::ENABLE_TABLES); + let parser = Parser::new_ext(markdown, opts); + let mut html_contents = String::new(); + html::push_html(&mut html_contents, parser); + + html_contents +} diff --git a/packages/tribble-app/src/templates/workflow/view.rs b/packages/tribble-app/src/templates/workflow/view.rs index c885c0e..8835e07 100644 --- a/packages/tribble-app/src/templates/workflow/view.rs +++ b/packages/tribble-app/src/templates/workflow/view.rs @@ -1,4 +1,5 @@ use super::get_build_state::WorkflowProps; +use super::parse_md::parse_md_to_html; use crate::parser::{ Endpoint, Input, InputSectionElem, InputType, Section, SectionElem, SelectOption, }; @@ -98,9 +99,9 @@ pub fn workflow_inner( RenderReportEndpoint(RenderReportEndpointProps { preamble: preamble.to_string(), text: text.to_string(), dest_text: dest_text.to_string(), dest_url: dest_url.to_string() }) }, Endpoint::Instructional(text) => { - let text = text.to_string(); + let text = parse_md_to_html(text); view! { - p { (text) } + div(class = "markdown", dangerously_set_inner_html = &text) {} } } } @@ -158,9 +159,9 @@ fn render_section( .map(cloned!(ctx => move |section_elem| { let rendered = match section_elem { SectionElem::Text(text) => { - let text = text.clone(); + let text = parse_md_to_html(text); view! { - p { (text) } + div(class = "markdown", dangerously_set_inner_html = &text) {} } }, SectionElem::Progression { text, link, tags } => { @@ -520,6 +521,7 @@ fn render_report_endpoint( }: RenderReportEndpointProps, ) -> View { let ctx = use_context::(); + let preamble = parse_md_to_html(&preamble); // Flatten the tags into one single vector let mut flattened_tags: Vec = Vec::new(); let history = ctx.history.get(); @@ -566,7 +568,7 @@ fn render_report_endpoint( }); view! { - p(class = "mb-2") { (preamble) } + div(class = "markdown mb-2", dangerously_set_inner_html = &preamble) {} // The report itself is preformatted pre(class = "group overflow-x-auto break-words whitespace-pre-wrap", tabindex = "0") { div(class = "relative") { diff --git a/packages/tribble-app/static/style.css b/packages/tribble-app/static/style.css index 5c74f09..3ffce1e 100644 --- a/packages/tribble-app/static/style.css +++ b/packages/tribble-app/static/style.css @@ -111,3 +111,53 @@ div.select-wrapper:not(div.select-multiple)::after { @apply bg-primary; @apply after:translate-x-4; } + +/* Markdown styling to combat Tailwind's removal of all default classes */ +.markdown > * + *, +.markdown li + li, +.markdown li > p + p { + @apply mt-2; +} +.markdown stong, +.markdown bold { + @apply font-black; +} +.markdown a { + @apply text-primary hover:text-dark transition-colors duration-200 underline; +} +.markdown h1 { + @apply text-4xl mt-2; +} +.markdown h2 { + @apply text-3xl; +} +.markdown h3 { + @apply text-2xl; +} +.markdown h4 { + @apply text-xl +} +.markdown h5 { + @apply text-lg; +} +.markdown h6 { + @apply leading-tight; +} +.markdown :not(pre) code { + @apply rounded-lg p-1 bg-neutral-200; +} +.markdown pre { + @apply rounded-lg overflow-x-auto; +} +.markdown blockquote { + @apply pl-4 italic border-l-4; +} +.markdown ul { + @apply list-disc pl-5 sm:pl-10; +} +.markdown ol { + @apply list-decimal pl-5 sm:pl-10; +} +.markdown details :not(summary) { + @apply ml-6; +} diff --git a/packages/tribble-app/tailwind.config.js b/packages/tribble-app/tailwind.config.js index 4ca5e6f..f0cee80 100644 --- a/packages/tribble-app/tailwind.config.js +++ b/packages/tribble-app/tailwind.config.js @@ -16,11 +16,13 @@ module.exports = { }, colors: { primary: "#50587C", + dark: "#282C3E", transparent: colors.transparent, neutral: colors.neutral, red: colors.red, bg: "#FFFFFF", - black: colors.black + black: colors.black, + white: colors.white } }, plugins: [],