FIX: Quoting local dates bbcode regeneration (#17141)

This commit allows quoting of discourse-local-date elements
and converts the quoted tags back into bbcode so that the
rendered quote will also render the discourse-local-date HTML.
This works on single dates as well as date ranges, and supports
all of the options used by discourse-local-date.

This also necessitated adding addTextDecorateCallback to the
to-markdown core lib (similar to addBlockDecorateCallback and
addTagDecorateCallback) to transform the text nodes between
date ranges to remove the -> in the final quote.

c.f. https://meta.discourse.org/t/quotes-that-contain-date-time/101999
This commit is contained in:
Martin Brennan 2022-06-21 10:07:21 +10:00 committed by GitHub
parent fd294a60cf
commit 54a518b21d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 331 additions and 44 deletions

View File

@ -6,9 +6,11 @@ const MSO_LIST_CLASSES = [
let tagDecorateCallbacks = []; let tagDecorateCallbacks = [];
let blockDecorateCallbacks = []; let blockDecorateCallbacks = [];
let textDecorateCallbacks = [];
/** /**
* Allows to add support for custom inline markdown/bbcode * Allows to add support for custom inline markdown/bbcode prefixes
* to convert nodes back to bbcode.
* *
* ``` * ```
* addTagDecorateCallback(function (text) { * addTagDecorateCallback(function (text) {
@ -29,7 +31,8 @@ export function clearTagDecorateCallbacks() {
} }
/** /**
* Allows to add support for custom block markdown/bbcode * Allows to add support for custom block markdown/bbcode prefixes
* to convert nodes back to bbcode.
* *
* ``` * ```
* addBlockDecorateCallback(function (text) { * addBlockDecorateCallback(function (text) {
@ -48,6 +51,30 @@ export function clearBlockDecorateCallbacks() {
blockDecorateCallbacks = []; blockDecorateCallbacks = [];
} }
/**
* Allows to add support for custom text node transformations
* based on the next/previous elements.
*
* ```
* addTextDecorateCallback(function (text, nextElement, previousElement) {
* if (
* startRangeOpts &&
* nextElement?.attributes.class?.includes("discourse-local-date") &&
* text === "→"
* ) {
* return "";
* }
* });
* ```
*/
export function addTextDecorateCallback(callback) {
textDecorateCallbacks.push(callback);
}
export function clearTextDecorateCallbacks() {
textDecorateCallbacks = [];
}
export class Tag { export class Tag {
static named(name) { static named(name) {
const klass = class NamedTag extends Tag {}; const klass = class NamedTag extends Tag {};
@ -657,9 +684,10 @@ function tagByName(name) {
} }
class Element { class Element {
constructor(element, parent, previous, next) { constructor(element, parent, previous, next, metadata) {
this.name = element.name; this.name = element.name;
this.data = element.data; this.data = element.data;
this.metadata = metadata;
this.children = element.children; this.children = element.children;
this.attributes = element.attributes; this.attributes = element.attributes;
@ -682,6 +710,7 @@ class Element {
tag() { tag() {
const tag = new (tagByName(this.name) || Tag)(); const tag = new (tagByName(this.name) || Tag)();
tag.element = this; tag.element = this;
tag.metadata = this.metadata;
return tag; return tag;
} }
@ -709,6 +738,19 @@ class Element {
} }
text = text.replace(/[\s\t]+/g, " "); text = text.replace(/[\s\t]+/g, " ");
textDecorateCallbacks.forEach((callback) => {
const result = callback.call(
this,
text,
this.next,
this.previous,
this.metadata
);
if (typeof result !== "undefined") {
text = result;
}
});
return text; return text;
} }
@ -721,8 +763,8 @@ class Element {
return this.parentNames.filter((p) => names.includes(p)); return this.parentNames.filter((p) => names.includes(p));
} }
static toMarkdown(element, parent, prev, next) { static toMarkdown(element, parent, prev, next, metadata) {
return new Element(element, parent, prev, next).toMarkdown(); return new Element(element, parent, prev, next, metadata).toMarkdown();
} }
static parseChildren(parent) { static parseChildren(parent) {
@ -732,12 +774,15 @@ class Element {
static parse(elements, parent = null) { static parse(elements, parent = null) {
if (elements) { if (elements) {
let result = []; let result = [];
let metadata = {};
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const prev = i === 0 ? null : elements[i - 1]; const prev = i === 0 ? null : elements[i - 1];
const next = i === elements.length ? null : elements[i + 1]; const next = i === elements.length ? null : elements[i + 1];
result.push(Element.toMarkdown(elements[i], parent, prev, next)); result.push(
Element.toMarkdown(elements[i], parent, prev, next, metadata)
);
} }
return result.join(""); return result.join("");

View File

@ -67,6 +67,7 @@ import { resetDefaultSectionLinks as resetTopicsSectionLinks } from "discourse/l
import { import {
clearBlockDecorateCallbacks, clearBlockDecorateCallbacks,
clearTagDecorateCallbacks, clearTagDecorateCallbacks,
clearTextDecorateCallbacks,
} from "discourse/lib/to-markdown"; } from "discourse/lib/to-markdown";
const LEGACY_ENV = !setupApplicationTest; const LEGACY_ENV = !setupApplicationTest;
@ -194,6 +195,7 @@ function testCleanup(container, app) {
resetTopicsSectionLinks(); resetTopicsSectionLinks();
clearTagDecorateCallbacks(); clearTagDecorateCallbacks();
clearBlockDecorateCallbacks(); clearBlockDecorateCallbacks();
clearTextDecorateCallbacks();
} }
export function discourseModule(name, options) { export function discourseModule(name, options) {

View File

@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Adds `beforeToMarkdownTagDecorate` and `beforeToMarkdownBlockDecorate` that allow to modify to-markdown behavior. - N/A - Mistakenly bumped.
## [1.2.0] - 2022-03-18 ## [1.2.0] - 2022-03-18

View File

@ -14,6 +14,7 @@ import { propertyNotEqual } from "discourse/lib/computed";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
import { applyLocalDates } from "discourse/lib/local-dates"; import { applyLocalDates } from "discourse/lib/local-dates";
import generateDateMarkup from "discourse/plugins/discourse-local-dates/lib/local-date-markup-generator";
export default Component.extend({ export default Component.extend({
timeFormat: "HH:mm:ss", timeFormat: "HH:mm:ss",
@ -262,43 +263,7 @@ export default Component.extend({
}, },
_generateDateMarkup(fromDateTime, options, isRange, toDateTime) { _generateDateMarkup(fromDateTime, options, isRange, toDateTime) {
let text = ``; return generateDateMarkup(fromDateTime, options, isRange, toDateTime);
if (isRange) {
let from = [fromDateTime.date, fromDateTime.time]
.filter((element) => !isEmpty(element))
.join("T");
let to = [toDateTime.date, toDateTime.time]
.filter((element) => !isEmpty(element))
.join("T");
text += `[date-range from=${from} to=${to}`;
} else {
text += `[date=${fromDateTime.date}`;
}
if (fromDateTime.time && !isRange) {
text += ` time=${fromDateTime.time}`;
}
if (fromDateTime.format && fromDateTime.format.length) {
text += ` format="${fromDateTime.format}"`;
}
if (options.timezone) {
text += ` timezone="${options.timezone}"`;
}
if (options.timezones && options.timezones.length) {
text += ` timezones="${options.timezones.join("|")}"`;
}
if (options.recurring && !isRange) {
text += ` recurring="${options.recurring}"`;
}
text += `]`;
return text;
}, },
@computed("advancedMode") @computed("advancedMode")

View File

@ -7,6 +7,11 @@ import { downloadCalendar } from "discourse/lib/download-calendar";
import { renderIcon } from "discourse-common/lib/icon-library"; import { renderIcon } from "discourse-common/lib/icon-library";
import I18n from "I18n"; import I18n from "I18n";
import { hidePopover, showPopover } from "discourse/lib/d-popover"; import { hidePopover, showPopover } from "discourse/lib/d-popover";
import {
addTagDecorateCallback,
addTextDecorateCallback,
} from "discourse/lib/to-markdown";
import generateDateMarkup from "discourse/plugins/discourse-local-dates/lib/local-date-markup-generator";
// Import applyLocalDates from discourse/lib/local-dates instead // Import applyLocalDates from discourse/lib/local-dates instead
export function applyLocalDates(dates, siteSettings) { export function applyLocalDates(dates, siteSettings) {
@ -66,6 +71,24 @@ function buildOptionsFromElement(element, siteSettings) {
return opts; return opts;
} }
function buildOptionsFromMarkdownTag(element) {
const opts = {};
// siteSettings defaults as used by buildOptionsFromElement are purposefully
// ommitted to reproduce exactly what was on the original element
opts.time = element.attributes["data-time"];
opts.date = element.attributes["data-date"];
opts.recurring = element.attributes["data-recurring"];
opts.timezones = element.attributes["data-timezones"];
opts.timezone = element.attributes["data-timezone"];
opts.calendar = (element.attributes["data-calendar"] || "on") === "on";
opts.displayedTimezone = element.attributes["data-displayed-timezone"];
opts.format = element.attributes["data-format"];
opts.countdown = element.attributes["data-countdown"];
return opts;
}
function _rangeElements(element) { function _rangeElements(element) {
if (!element.parentElement) { if (!element.parentElement) {
return []; return [];
@ -128,6 +151,60 @@ function initializeDiscourseLocalDates(api) {
}, },
}, },
}); });
addTextDecorateCallback(function (
text,
nextElement,
_previousElement,
metadata
) {
if (
metadata.discourseLocalDateStartRangeOpts &&
nextElement?.attributes.class?.includes("discourse-local-date") &&
text === "→"
) {
return "";
}
});
addTagDecorateCallback(function () {
if (this.element.attributes.class?.includes("discourse-local-date")) {
if (this.metadata.discourseLocalDateStartRangeOpts) {
const startRangeOpts = this.metadata.discourseLocalDateStartRangeOpts;
const endRangeOpts = buildOptionsFromMarkdownTag(this.element);
const markup = generateDateMarkup(
{
date: startRangeOpts.date,
time: startRangeOpts.time,
format: startRangeOpts.format,
},
endRangeOpts,
true,
{
date: endRangeOpts.date,
time: endRangeOpts.time,
format: endRangeOpts.format,
}
);
this.prefix = markup;
this.metadata.discourseLocalDateStartRangeOpts = null;
return "";
}
if (this.element.attributes["data-range"] === "true") {
this.metadata.discourseLocalDateStartRangeOpts = buildOptionsFromMarkdownTag(
this.element
);
return "";
}
const opts = buildOptionsFromMarkdownTag(this.element, siteSettings);
const markup = generateDateMarkup(
{ date: opts.date, time: opts.time, format: opts.format },
opts,
false
);
this.prefix = markup;
return "";
}
});
} }
function buildHtmlPreview(element, siteSettings) { function buildHtmlPreview(element, siteSettings) {

View File

@ -0,0 +1,58 @@
import { isEmpty } from "@ember/utils";
export default function generateDateMarkup(
fromDateTime,
options,
isRange,
toDateTime
) {
let text = ``;
if (isRange) {
let from = [fromDateTime.date, fromDateTime.time]
.filter((element) => !isEmpty(element))
.join("T");
let to = [toDateTime.date, toDateTime.time]
.filter((element) => !isEmpty(element))
.join("T");
text += `[date-range from=${from} to=${to}`;
} else {
text += `[date=${fromDateTime.date}`;
}
if (fromDateTime.time && !isRange) {
text += ` time=${fromDateTime.time}`;
}
if (fromDateTime.format && fromDateTime.format.length) {
text += ` format="${fromDateTime.format}"`;
}
if (options.timezone) {
text += ` timezone="${options.timezone}"`;
}
if (options.countdown) {
text += ` countdown="${options.countdown}"`;
}
if (options.displayedTimezone) {
text += ` displayedTimezone="${options.displayedTimezone}"`;
}
if (options.timezones && options.timezones.length) {
if (Array.isArray(options.timezones)) {
text += ` timezones="${options.timezones.join("|")}"`;
} else {
text += ` timezones="${options.timezones}"`;
}
}
if (options.recurring && !isRange) {
text += ` recurring="${options.recurring}"`;
}
text += `]`;
return text;
}

View File

@ -0,0 +1,140 @@
import { cloneJSON } from "discourse-common/lib/object";
import topicFixtures from "discourse/tests/fixtures/topic";
import {
acceptance,
queryAll,
selectText,
} from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { click, visit } from "@ember/test-helpers";
acceptance("Local Dates - quoting", function (needs) {
needs.user();
needs.settings({ discourse_local_dates_enabled: true });
needs.pretender((server, helper) => {
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
const firstPost = topicResponse.post_stream.posts[0];
firstPost.cooked += `<div class='select-local-date-test'>This is a test <span data-date="2022-06-17" data-time="10:00:00" class="discourse-local-date cooked-date past" data-displayed-timezone="Australia/Perth" data-timezone="Australia/Brisbane" data-email-preview="2022-06-17T00:00:00Z UTC" aria-label="Brisbane Friday, June 17, 2022
<br />
<svg class='fa d-icon d-icon-clock svg-icon svg-string'
xmlns=&quot;http://www.w3.org/2000/svg&quot;>
<use href=&quot;#clock&quot; />
</svg> 10:00 AM, Paris Friday, June 17, 2022
<br />
<svg class='fa d-icon d-icon-clock svg-icon svg-string'
xmlns=&quot;http://www.w3.org/2000/svg&quot;>
<use href=&quot;#clock&quot; />
</svg> 2:00 AM, Los Angeles Thursday, June 16, 2022
<br />
<svg class='fa d-icon d-icon-clock svg-icon svg-string'
xmlns=&quot;http://www.w3.org/2000/svg&quot;>
<use href=&quot;#clock&quot; />
</svg> 5:00 PM" data-title="This is a new topic to check on chat quote issues">
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
<use href="#globe-americas"></use>
</svg>
<span class="relative-time">June 17, 2022 8:00 AM (Perth)</span>
</span></div>`;
server.get("/t/280.json", () => helper.response(topicResponse));
server.get("/t/280/:post_number.json", () => {
helper.response(topicResponse);
});
});
test("quoting single local dates with basic options", async function (assert) {
await visit("/t/internationalization-localization/280");
await selectText("#post_1 .select-local-date-test");
await click(".insert-quote");
assert.strictEqual(
queryAll(".d-editor-input").val().trim(),
`[quote=\"Uwe Keim, post:1, topic:280, username:uwe_keim\"]
This is a test [date=2022-06-17 time=10:00:00 timezone="Australia/Brisbane" displayedTimezone="Australia/Perth"]
[/quote]`,
"converts the date to markdown with all options correctly"
);
});
});
acceptance("Local Dates - quoting range", function (needs) {
needs.user();
needs.settings({ discourse_local_dates_enabled: true });
needs.pretender((server, helper) => {
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
const firstPost = topicResponse.post_stream.posts[0];
firstPost.cooked += `<div class='select-local-date-test'><p dir="ltr">Some text <span data-date="2022-06-17" data-time="09:30:00" class="discourse-local-date cooked-date past" data-format="LL" data-range="true" data-timezones="Africa/Accra|Australia/Brisbane|Europe/Paris" data-timezone="Australia/Brisbane" data-email-preview="2022-06-16T23:30:00Z UTC" aria-label="Brisbane Friday, June 17, 2022 9:30 AM → Saturday, June 18, 2022 10:30 AM, Accra Thursday, June 16, 2022 11:30 PM → Saturday, June 18, 2022 12:30 AM, Paris Friday, June 17, 2022 1:30 AM → Saturday, June 18, 2022 2:30 AM" data-title="This is a new topic to check on chat quote issues">
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
<use href="#globe-americas"></use>
</svg>
<span class="relative-time">June 17, 2022</span>
</span><span data-date="2022-06-18" data-time="10:30:00" class="discourse-local-date cooked-date past" data-format="LL" data-range="true" data-timezones="Africa/Accra|Australia/Brisbane|Europe/Paris" data-timezone="Australia/Brisbane" data-email-preview="2022-06-18T00:30:00Z UTC" aria-label="Brisbane Friday, June 17, 2022 9:30 AM Saturday, June 18, 2022 10:30 AM, Accra Thursday, June 16, 2022 11:30 PM Saturday, June 18, 2022 12:30 AM, Paris Friday, June 17, 2022 1:30 AM Saturday, June 18, 2022 2:30 AM" data-title="This is a new topic to check on chat quote issues">
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
<use href="#globe-americas"></use>
</svg>
<span class="relative-time">June 18, 2022</span>
</span></p></div>`;
server.get("/t/280.json", () => helper.response(topicResponse));
server.get("/t/280/:post_number.json", () => {
helper.response(topicResponse);
});
});
test("quoting a range of local dates", async function (assert) {
await visit("/t/internationalization-localization/280");
await selectText("#post_1 .select-local-date-test");
await click(".insert-quote");
assert.strictEqual(
queryAll(".d-editor-input").val().trim(),
`[quote=\"Uwe Keim, post:1, topic:280, username:uwe_keim\"]
Some text [date-range from=2022-06-17T09:30:00 to=2022-06-18T10:30:00 format="LL" timezone="Australia/Brisbane" timezones="Africa/Accra|Australia/Brisbane|Europe/Paris"]
[/quote]`,
"converts the date range to markdown with all options correctly"
);
});
});
acceptance("Local Dates - quoting with recurring and countdown", function (needs) {
needs.user();
needs.settings({ discourse_local_dates_enabled: true });
needs.pretender((server, helper) => {
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
const firstPost = topicResponse.post_stream.posts[0];
firstPost.cooked += `<div class='select-local-date-test'><p dir="ltr">Testing countdown <span data-date="2022-06-21" data-time="09:30:00" class="discourse-local-date cooked-date" data-format="LL" data-countdown="true" data-timezone="Australia/Brisbane" data-email-preview="2022-06-20T23:30:00Z UTC" aria-label="Brisbane Tuesday, June 21, 2022 <br /><svg class='fa d-icon d-icon-clock svg-icon svg-string' xmlns=&quot;http://www.w3.org/2000/svg&quot;><use href=&quot;#clock&quot; /></svg> 9:30 AM, Paris Tuesday, June 21, 2022 <br /><svg class='fa d-icon d-icon-clock svg-icon svg-string' xmlns=&quot;http://www.w3.org/2000/svg&quot;><use href=&quot;#clock&quot; /></svg> 1:30 AM, Los Angeles Monday, June 20, 2022 <br /><svg class='fa d-icon d-icon-clock svg-icon svg-string' xmlns=&quot;http://www.w3.org/2000/svg&quot;><use href=&quot;#clock&quot; /></svg> 4:30 PM" data-title="This is a new topic to check on chat quote issues">
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
<use href="#globe-americas"></use>
</svg>
<span class="relative-time">21 hours</span>
</span></p>
<p dir="ltr">Testing recurring <span data-date="2022-06-22" class="discourse-local-date cooked-date" data-timezone="Australia/Brisbane" data-recurring="2.weeks" data-email-preview="2022-06-21T14:00:00Z UTC" aria-label="Brisbane Wednesday, June 22, 2022 12:00 AM → Thursday, June 23, 2022 12:00 AM, Paris Tuesday, June 21, 2022 4:00 PM → Wednesday, June 22, 2022 4:00 PM, Los Angeles Tuesday, June 21, 2022 7:00 AM → Wednesday, June 22, 2022 7:00 AM" data-title="This is a new topic to check on chat quote issues">
<svg class="fa d-icon d-icon-globe-americas svg-icon" xmlns="http://www.w3.org/2000/svg">
<use href="#globe-americas"></use>
</svg>
<span class="relative-time">Wednesday</span>
</span></p></div>`;
server.get("/t/280.json", () => helper.response(topicResponse));
server.get("/t/280/:post_number.json", () => {
helper.response(topicResponse);
});
});
test("quoting single local dates with recurring and countdown options", async function (assert) {
await visit("/t/internationalization-localization/280");
await selectText("#post_1 .select-local-date-test");
await click(".insert-quote");
assert.strictEqual(
queryAll(".d-editor-input").val().trim(),
`[quote=\"Uwe Keim, post:1, topic:280, username:uwe_keim\"]
Testing countdown [date=2022-06-21 time=09:30:00 format="LL" timezone="Australia/Brisbane" countdown="true"]
Testing recurring [date=2022-06-22 timezone="Australia/Brisbane" recurring="2.weeks"]
[/quote]`,
"converts the dates to markdown with all options correctly"
);
});
});