FEATURE: generic theme component bbcode wrapper (#7400)

FEATURE: generic theme component bbcode wrapper (#7400)

Usage:

[wrap=name foo=bar]
hello world
[/wrap]
diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js
index eba760f..6f88c5a 100644
--- a/app/assets/javascripts/markdown-it-bundle.js
+++ b/app/assets/javascripts/markdown-it-bundle.js
@@ -16,3 +16,4 @@
 //= require ./pretty-text/engines/discourse-markdown/text-post-process
 //= require ./pretty-text/engines/discourse-markdown/image-protocol
 //= require ./pretty-text/engines/discourse-markdown/inject-line-number
+//= require ./pretty-text/engines/discourse-markdown/d-wrap
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/d-wrap.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/d-wrap.js.es6
new file mode 100644
index 0000000..4b4b4ce
--- /dev/null
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/d-wrap.js.es6
@@ -0,0 +1,66 @@
+import { parseBBCodeTag } from "pretty-text/engines/discourse-markdown/bbcode-block";
+
+const WRAP_CLASS = "d-wrap";
+
+function parseAttributes(tagInfo) {
+  const attributes = tagInfo.attrs._default || "";
+
+  return (
+    parseBBCodeTag(`[wrap wrap=${attributes}]`, 0, attributes.length + 12)
+      .attrs || {}
+  );
+}
+
+function applyDataAttributes(token, state, attributes) {
+  Object.keys(attributes).forEach(tag => {
+    const value = state.md.utils.escapeHtml(attributes[tag]);
+    tag = state.md.utils.escapeHtml(tag.replace(/[^a-z0-9\-]/g, ""));
+
+    if (value && tag && tag.length > 1) {
+      token.attrs.push([`data-${tag}`, value]);
+    }
+  });
+}
+
+const blockRule = {
+  tag: "wrap",
+
+  before(state, tagInfo) {
+    let token = state.push("wrap_open", "div", 1);
+    token.attrs = [["class", WRAP_CLASS]];
+
+    applyDataAttributes(token, state, parseAttributes(tagInfo));
+  },
+
+  after(state) {
+    state.push("wrap_close", "div", -1);
+  }
+};
+
+const inlineRule = {
+  tag: "wrap",
+
+  replace(state, tagInfo, content) {
+    let token = state.push("wrap_open", "span", 1);
+    token.attrs = [["class", WRAP_CLASS]];
+
+    applyDataAttributes(token, state, parseAttributes(tagInfo));
+
+    if (content) {
+      token = state.push("text", "", 0);
+      token.content = content;
+    }
+
+    token = state.push("wrap_close", "span", -1);
+    return true;
+  }
+};
+
+export function setup(helper) {
+  helper.registerPlugin(md => {
+    md.inline.bbcode.ruler.push("inline-wrap", inlineRule);
+    md.block.bbcode.ruler.push("block-wrap", blockRule);
+  });
+
+  helper.whiteList([`div.${WRAP_CLASS}`, `span.${WRAP_CLASS}`, "span[data-*]"]);
+}
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 902f804..b76a253 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -1325,4 +1325,94 @@ HTML
     expect(cooked).to eq(html)
   end
 
+  describe "d-wrap" do
+    it "wraps the [wrap] tag inline" do
+      cooked = PrettyText.cook("[wrap=toc]taco[/wrap]")
+
+      html = <<~HTML
+        <div class="d-wrap" data-wrap="toc">
+        <p>taco</p>
+        </div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+
+      cooked = PrettyText.cook("Hello [wrap=toc id=1]taco[/wrap] world")
+
+      html = <<~HTML
+        <p>Hello <span class="d-wrap" data-wrap="toc" data-id="1">taco</span> world</p>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+
+    it "wraps the [wrap] tag in block" do
+      md = <<~MD
+        [wrap=toc]
+        taco
+        [/wrap]
+      MD
+
+      cooked = PrettyText.cook(md)
+
+      html = <<~HTML
+        <div class="d-wrap" data-wrap="toc">
+        <p>taco</p>
+        </div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+
+    it "wraps the [wrap] tag without content" do
+      md = <<~MD
+        [wrap=toc]
+        [/wrap]
+      MD
+
+      cooked = PrettyText.cook(md)
+
+      html = <<~HTML
+        <div class="d-wrap" data-wrap="toc"></div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+
+    it "adds attributes as data-attributes" do
+      cooked = PrettyText.cook("[wrap=toc name=\"pepper bell\" id=1]taco[/wrap]")
+
+      html = <<~HTML
+        <div class="d-wrap" data-wrap="toc" data-name="pepper bell" data-id="1">
+        <p>taco</p>
+        </div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+
+    it "prevents xss" do
+      cooked = PrettyText.cook('[wrap=toc foo="<script>console.log(1)</script>"]taco[/wrap]')
+
+      html = <<~HTML
+        <div class="d-wrap" data-wrap="toc" data-foo="&amp;lt;script&amp;gt;console.log(1)&amp;lt;/script&amp;gt;">
+        <p>taco</p>
+        </div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+
+    it "allows a limited set of attributes chars" do
+      cooked = PrettyText.cook('[wrap=toc fo@"èk-"!io=bar]taco[/wrap]')
+
+      html = <<~HTML
+        <div class=\"d-wrap\" data-wrap=\"toc\" data-io=\"bar\">
+        <p>taco</p>
+        </div>
+      HTML
+
+      expect(cooked).to eq(html.strip)
+    end
+  end
 end

GitHub sha: 7334362b

3 Likes