markdown: Clean up API for future reuse.

This gets us closer to having an API that can
be used my mobile.

The parse() function becomes a subset of
apply_markdown() that is no longer coupled
to the shape of a webapp object, and it can
be supplied with a new helper_config for each
invocation. Mobile will likely call this directly.

The setup() function becomes a subset of
initialize() that allows you to set up the
parser **before** having to build any kind of
message-specific helpers. Mobile will likely
call this directly.

The webapp continues to call these functions,
which are now thin wrappers:

    * apply_markdown (wrapping parse)
    * initialize (wrapping setup)

Note we still have several other problems to
solve before mobile can use this code, but we
introduce this now so that we can get a head
start on prototyping and unit testing.

Also, this commit does not address the fact
that contains_backend_only_syntax() is still
bound to the webapp config.
This commit is contained in:
Steve Howell
2022-03-30 12:20:41 +00:00
committed by Tim Abbott
parent 935cb605a5
commit 8d9e6d6b87
2 changed files with 190 additions and 8 deletions

View File

@@ -0,0 +1,149 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const markdown = zrequire("markdown");
const my_id = 101;
const user_map = new Map();
user_map.set(my_id, "Me Myself");
user_map.set(105, "greg");
function get_actual_name_from_user_id(user_id) {
return user_map.get(user_id);
}
function get_user_id_from_name(name) {
for (const [user_id, _name] of user_map.entries()) {
if (name === _name) {
return user_id;
}
}
return undefined;
}
function is_valid_full_name_and_user_id(name, user_id) {
return user_map.has(user_id) && user_map.get(user_id) === name;
}
function my_user_id() {
return my_id;
}
function is_valid_user_id(user_id) {
return user_map.has(user_id);
}
const staff_group = {
id: 201,
name: "Staff",
};
const user_group_map = new Map();
user_group_map.set(staff_group.name, staff_group);
function get_user_group_from_name(name) {
return user_group_map.get(name);
}
function is_member_of_user_group(user_group_id, user_id) {
assert.equal(user_group_id, staff_group.id);
assert.equal(user_id, my_id);
return true;
}
const social = {
stream_id: 301,
name: "social",
};
const sub_map = new Map();
sub_map.set(social.name, social);
function get_stream_by_name(name) {
return sub_map.get(name);
}
function stream_hash(stream_id) {
return `stream-${stream_id}`;
}
function stream_topic_hash(stream_id, topic) {
return `stream-${stream_id}-topic-${topic}`;
}
const helper_config = {
// user stuff
get_actual_name_from_user_id,
get_user_id_from_name,
is_valid_full_name_and_user_id,
is_valid_user_id,
my_user_id,
// user groups
get_user_group_from_name,
is_member_of_user_group,
// stream hashes
get_stream_by_name,
stream_hash,
stream_topic_hash,
// settings
should_translate_emoticons: () => false,
};
function assert_parse(raw_content, expected_content) {
const {content} = markdown.parse({raw_content, helper_config});
assert.equal(content, expected_content);
}
function test(label, f) {
markdown.setup();
run_test(label, f);
}
test("basics", () => {
assert_parse("boring", "<p>boring</p>");
assert_parse("**bold**", "<p><strong>bold</strong></p>");
});
test("user mentions", () => {
assert_parse("@**greg**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse("@**|105**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse(
"@**greg|105**",
'<p><span class="user-mention" data-user-id="105">@greg</span></p>',
);
assert_parse(
"@**Me Myself|101**",
'<p><span class="user-mention" data-user-id="101">@Me Myself</span></p>',
);
});
test("user group mentions", () => {
assert_parse(
"@*Staff*",
'<p><span class="user-group-mention" data-user-group-id="201">@Staff</span></p>',
);
});
test("stream links", () => {
assert_parse(
"#**social**",
'<p><a class="stream" data-stream-id="301" href="/stream-301">#social</a></p>',
);
assert_parse(
"#**social>lunch**",
'<p><a class="stream-topic" data-stream-id="301" href="/stream-301-topic-lunch">#social &gt; lunch</a></p>',
);
});

View File

@@ -94,7 +94,14 @@ export function contains_backend_only_syntax(content) {
return markedup !== undefined || false_linkifier_match !== undefined;
}
export function apply_markdown(message) {
export function parse({raw_content, helper_config}) {
// Given the raw markdown content of a message (raw_content)
// we return the HTML content (content) and flags.
// Our caller passes a helper_config object that has several
// helper functions for getting info about users, streams, etc.
helpers = helper_config;
let mentioned = false;
let mentioned_group = false;
let mentioned_wildcard = false;
@@ -249,20 +256,20 @@ export function apply_markdown(message) {
};
// Our Python-Markdown processor appends two \n\n to input
message.content = marked(message.raw_content + "\n\n", options).trim();
const content = marked(raw_content + "\n\n", options).trim();
// Simulate message flags for our locally rendered
// message. Messages the user themselves sent via the browser are
// always marked as read.
message.flags = ["read"];
const flags = ["read"];
if (mentioned || mentioned_group) {
message.flags.push("mentioned");
flags.push("mentioned");
}
if (mentioned_wildcard) {
message.flags.push("wildcard_mentioned");
flags.push("wildcard_mentioned");
}
message.is_me_message = is_status_message(message.raw_content);
return {content, flags};
}
export function add_topic_links(message) {
@@ -422,8 +429,9 @@ function handleTex(tex, fullmatch) {
}
}
export function initialize(helper_config) {
helpers = helper_config;
export function setup() {
// Once we focus on supporting other platforms such as mobile,
// we will export this function.
function disable_markdown_regex(rules, name) {
rules[name] = {
@@ -504,3 +512,28 @@ export function initialize(helper_config) {
preprocessors: [preprocess_code_blocks, preprocess_translate_emoticons],
});
}
// NOTE: Everything below this line is likely to be webapp-specific
// and won't be used by future platforms such as mobile.
// We may eventually move this code to a new file, but we want
// to wait till the dust settles a bit on some other changes first.
let webapp_helpers;
export function initialize(helper_config) {
// This is generally only intended to be called by the webapp. Most
// other platforms should call setup().
webapp_helpers = helper_config;
helpers = helper_config;
setup();
}
export function apply_markdown(message) {
// This is generally only intended to be called by the webapp. Most
// other platforms should call parse().
const raw_content = message.raw_content;
const {content, flags} = parse({raw_content, helper_config: webapp_helpers});
message.content = content;
message.flags = flags;
message.is_me_message = is_status_message(raw_content);
}