FEATURE: Customizable rules and plugins for `PrettyText.markdown`.

This commit extends the options which can be passed to
`PrettyText.markdown` so that which Markdown-it rules and Discourse
Markdown plugins to be used when rendering a text can be customizable.
Currently, this extension is mainly used by plugins.
This commit is contained in:
Alan Guo Xiang Tan 2022-01-06 15:27:12 +08:00
parent 9f5c8644d0
commit c2afc3915b
5 changed files with 102 additions and 8 deletions

View File

@ -1493,6 +1493,45 @@ var bar = 'bar';
); );
}); });
test("customizing markdown-it rules", function (assert) {
assert.cookedOptions(
"**bold**",
{ markdownItRules: [] },
"<p>**bold**</p>",
"does not apply bold markdown when rule is not enabled"
);
assert.cookedOptions(
"**bold**",
{ markdownItRules: ["emphasis"] },
"<p><strong>bold</strong></p>",
"applies bold markdown when rule is enabled"
);
});
test("features override", function (assert) {
assert.cookedOptions(
":grin: @sam",
{ featuresOverride: [] },
"<p>:grin: @sam</p>",
"does not cook emojis when Discourse markdown engines are disabled"
);
assert.cookedOptions(
":grin: @sam",
{ featuresOverride: ["emoji"] },
'<p><img src="/images/emoji/google_classic/grin.png?v=10" title=":grin:" class="emoji" alt=":grin:"> @sam</p>',
"cooks emojis when only the emoji markdown engine is enabled"
);
assert.cookedOptions(
":grin: @sam",
{ featuresOverride: ["mentions", "text-post-process"] },
`<p>:grin: <span class="mention">@sam</span></p>`,
"cooks mentions when only the mentions markdown engine is enabled"
);
});
test("emoji", function (assert) { test("emoji", function (assert) {
assert.cooked( assert.cooked(
":smile:", ":smile:",

View File

@ -353,6 +353,12 @@ export function setup(opts, siteSettings, state) {
} }
}); });
if (opts.featuresOverride) {
Object.keys(opts.features).forEach((feature) => {
opts.features[feature] = opts.featuresOverride.includes(feature);
});
}
let copy = {}; let copy = {};
Object.keys(opts).forEach((entry) => { Object.keys(opts).forEach((entry) => {
copy[entry] = opts[entry]; copy[entry] = opts[entry];
@ -371,14 +377,22 @@ export function setup(opts, siteSettings, state) {
enableDiffhtmlPreview: siteSettings.enable_diffhtml_preview, enableDiffhtmlPreview: siteSettings.enable_diffhtml_preview,
}; };
opts.engine = window.markdownit({ const markdownitOpts = {
discourse: opts.discourse, discourse: opts.discourse,
html: true, html: true,
breaks: !siteSettings.traditional_markdown_linebreaks, breaks: !siteSettings.traditional_markdown_linebreaks,
xhtmlOut: false, xhtmlOut: false,
linkify: siteSettings.enable_markdown_linkify, linkify: siteSettings.enable_markdown_linkify,
typographer: siteSettings.enable_markdown_typographer, typographer: siteSettings.enable_markdown_typographer,
}); };
if (opts.discourse.markdownItRules !== undefined) {
opts.engine = window
.markdownit("zero", markdownitOpts) // Preset for "zero", https://github.com/markdown-it/markdown-it/blob/master/lib/presets/zero.js
.enable(opts.discourse.markdownItRules);
} else {
opts.engine = window.markdownit(markdownitOpts);
}
const quotation_marks = siteSettings.markdown_typographer_quotation_marks; const quotation_marks = siteSettings.markdown_typographer_quotation_marks;
if (quotation_marks) { if (quotation_marks) {

View File

@ -38,6 +38,8 @@ export function buildOptions(state) {
customEmojiTranslation, customEmojiTranslation,
watchedWordsReplace, watchedWordsReplace,
watchedWordsLink, watchedWordsLink,
featuresOverride,
markdownItRules,
} = state; } = state;
let features = {}; let features = {};
@ -76,6 +78,8 @@ export function buildOptions(state) {
disableEmojis, disableEmojis,
watchedWordsReplace, watchedWordsReplace,
watchedWordsLink, watchedWordsLink,
featuresOverride,
markdownItRules,
}; };
// note, this will mutate options due to the way the API is designed // note, this will mutate options due to the way the API is designed

View File

@ -156,6 +156,17 @@ module PrettyText
end end
end end
# Acceptable options:
#
# disable_emojis - Disables the emoji markdown engine.
# features - A hash where the key is the markdown feature name and the value is a boolean to enable/disable the markdown feature.
# The hash is merged into the default features set in pretty-text.js which can be used to add new features or disable existing features.
# features_override - An array of markdown feature names to override the default markdown feature set. Currently used by plugins to customize what features should be enabled
# when rendering markdown.
# markdown_it_rules - An array of markdown rule names which will be applied to the markdown-it engine. Currently used by plugins to customize what markdown-it rules should be
# enabled when rendering markdown.
# topic_id - Topic id for the post being cooked.
# user_id - User id for the post being cooked.
def self.markdown(text, opts = {}) def self.markdown(text, opts = {})
# we use the exact same markdown converter as the client # we use the exact same markdown converter as the client
# TODO: use the same extensions on both client and server (in particular the template for mentions) # TODO: use the same extensions on both client and server (in particular the template for mentions)
@ -175,6 +186,8 @@ module PrettyText
__paths = #{paths_json}; __paths = #{paths_json};
__optInput.getURL = __getURL; __optInput.getURL = __getURL;
#{"__optInput.features = #{opts[:features].to_json};" if opts[:features]} #{"__optInput.features = #{opts[:features].to_json};" if opts[:features]}
#{"__optInput.featuresOverride = #{opts[:features_override].to_json};" if opts[:features_override]}
#{"__optInput.markdownItRules = #{opts[:markdown_it_rules].to_json};" if opts[:markdown_it_rules]}
__optInput.getCurrentUser = __getCurrentUser; __optInput.getCurrentUser = __getCurrentUser;
__optInput.lookupAvatar = __lookupAvatar; __optInput.lookupAvatar = __lookupAvatar;
__optInput.lookupPrimaryUserGroup = __lookupPrimaryUserGroup; __optInput.lookupPrimaryUserGroup = __lookupPrimaryUserGroup;
@ -190,8 +203,8 @@ module PrettyText
__optInput.watchedWordsLink = #{WordWatcher.word_matcher_regexps(:link).to_json}; __optInput.watchedWordsLink = #{WordWatcher.word_matcher_regexps(:link).to_json};
JS JS
if opts[:topicId] if opts[:topic_id]
buffer << "__optInput.topicId = #{opts[:topicId].to_i};\n" buffer << "__optInput.topicId = #{opts[:topic_id].to_i};\n"
end end
if opts[:user_id] if opts[:user_id]
@ -279,10 +292,6 @@ module PrettyText
def self.cook(text, opts = {}) def self.cook(text, opts = {})
options = opts.dup options = opts.dup
# we have a minor inconsistency
options[:topicId] = opts[:topic_id]
working_text = text.dup working_text = text.dup
sanitized = markdown(working_text, options) sanitized = markdown(working_text, options)

View File

@ -2072,4 +2072,32 @@ HTML
expect(cooked).to match_html(html) expect(cooked).to match_html(html)
end end
context "customizing markdown-it rules" do
it 'customizes the markdown-it rules correctly' do
cooked = PrettyText.cook('This is some text **bold**', markdown_it_rules: [])
expect(cooked).to eq("<p>This is some text **bold**</p>")
cooked = PrettyText.cook('This is some text **bold**', markdown_it_rules: ["emphasis"])
expect(cooked).to eq("<p>This is some text <strong>bold</strong></p>")
end
end
context "enabling/disabling features" do
it "allows features to be overriden" do
cooked = PrettyText.cook(':grin: @mention', features_override: [])
expect(cooked).to eq("<p>:grin: @mention</p>")
cooked = PrettyText.cook(':grin: @mention', features_override: ["emoji"])
expect(cooked).to eq("<p><img src=\"/images/emoji/twitter/grin.png?v=10\" title=\":grin:\" class=\"emoji\" alt=\":grin:\"> @mention</p>")
cooked = PrettyText.cook(':grin: @mention', features_override: ["mentions", "text-post-process"])
expect(cooked).to eq("<p>:grin: <span class=\"mention\">@mention</span></p>")
end
end
end end