FEATURE: date-range tag for local dates (#15474)

New range tag for local dates with syntax like:
```
[date-range from=2022-01-06T13:00 to=2022-01-08 timezone=Australia/Sydney]
```

Previously, 2 dates in one line were considered as range. It was hard to decide if 2 dates are range when they were in separate lines or have some content between them.

New explicit tag should clearly distinguish between single date and range.

Common code from `addLocalDate` is extracted to `addSingleLocalDate`.

Both `addLocalDate` and new `addLocalRange` are using `addSingleLocalDate`.

Also, `defaultDateConfig` was extracted to have one place for all possible parameters.
This commit is contained in:
Krzysztof Kotlarek 2022-01-10 08:02:36 +01:00 committed by GitHub
parent fff8b98485
commit 17ec3bc5b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 42 deletions

View File

@ -258,15 +258,27 @@ export default Component.extend({
]; ];
}, },
_generateDateMarkup(config, options, isRange) { _generateDateMarkup(fromDateTime, options, isRange, toDateTime) {
let text = `[date=${config.date}`; let text = ``;
if (config.time) { if (isRange) {
text += ` time=${config.time}`; 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 (config.format && config.format.length) { if (fromDateTime.time && !isRange) {
text += ` format="${config.format}"`; text += ` time=${fromDateTime.time}`;
}
if (fromDateTime.format && fromDateTime.format.length) {
text += ` format="${fromDateTime.format}"`;
} }
if (options.timezone) { if (options.timezone) {
@ -298,11 +310,15 @@ export default Component.extend({
let text; let text;
if (isValid && config.from) { if (isValid && config.from) {
text = this._generateDateMarkup(config.from, options, isRange);
if (config.to && config.to.range) { if (config.to && config.to.range) {
text += ``; text = this._generateDateMarkup(
text += this._generateDateMarkup(config.to, options, isRange); config.from,
options,
isRange,
config.to
);
} else {
text = this._generateDateMarkup(config.from, options, isRange);
} }
} }

View File

@ -69,6 +69,16 @@ function _rangeElements(element) {
if (!element.parentElement) { if (!element.parentElement) {
return []; return [];
} }
// TODO: element.parentElement.children.length !== 2 is a fallback to old solution for ranges
// Condition can be removed after migration to [date-range]
if (
element.dataset.range !== "true" &&
element.parentElement.children.length !== 2
) {
return [element];
}
return Array.from(element.parentElement.children).filter( return Array.from(element.parentElement.children).filter(
(span) => span.dataset.date (span) => span.dataset.date
); );

View File

@ -2,38 +2,8 @@ import { parseBBCodeTag } from "pretty-text/engines/discourse-markdown/bbcode-bl
const timezoneNames = moment.tz.names(); const timezoneNames = moment.tz.names();
function addLocalDate(buffer, matches, state) { function addSingleLocalDate(buffer, state, config) {
let token; let token = new state.Token("span_open", "span", 1);
let config = {
date: null,
time: null,
timezone: null,
format: null,
timezones: null,
displayedTimezone: null,
countdown: null,
};
const matchString = matches[1].replace(/||„|“|«|»|”/g, '"');
let parsed = parseBBCodeTag(
"[date date" + matchString + "]",
0,
matchString.length + 11
);
config.date = parsed.attrs.date;
config.format = parsed.attrs.format;
config.calendar = parsed.attrs.calendar;
config.time = parsed.attrs.time;
config.timezone = (parsed.attrs.timezone || "").trim();
config.recurring = parsed.attrs.recurring;
config.timezones = parsed.attrs.timezones;
config.displayedTimezone = parsed.attrs.displayedTimezone;
config.countdown = parsed.attrs.countdown;
token = new state.Token("span_open", "span", 1);
token.attrs = [["data-date", state.md.utils.escapeHtml(config.date)]]; token.attrs = [["data-date", state.md.utils.escapeHtml(config.date)]];
if (!config.date.match(/\d{4}-\d{2}-\d{2}/)) { if (!config.date.match(/\d{4}-\d{2}-\d{2}/)) {
@ -76,6 +46,9 @@ function addLocalDate(buffer, matches, state) {
state.md.utils.escapeHtml(config.calendar), state.md.utils.escapeHtml(config.calendar),
]); ]);
} }
if (config.range) {
token.attrs.push(["data-range", true]);
}
if ( if (
config.displayedTimezone && config.displayedTimezone &&
@ -127,6 +100,77 @@ function addLocalDate(buffer, matches, state) {
closeBuffer(buffer, state, dateTime.utc().format(config.format)); closeBuffer(buffer, state, dateTime.utc().format(config.format));
} }
function defaultDateConfig() {
return {
date: null,
time: null,
timezone: null,
format: null,
timezones: null,
displayedTimezone: null,
countdown: null,
range: false,
};
}
function parseTagAttributes(tag) {
const matchString = tag.replace(/||„|“|«|»|”/g, '"');
return parseBBCodeTag(
"[date date" + matchString + "]",
0,
matchString.length + 12
);
}
function addLocalDate(buffer, matches, state) {
let config = defaultDateConfig();
const parsed = parseTagAttributes(matches[1]);
config.date = parsed.attrs.date;
config.format = parsed.attrs.format;
config.calendar = parsed.attrs.calendar;
config.time = parsed.attrs.time;
config.timezone = (parsed.attrs.timezone || "").trim();
config.recurring = parsed.attrs.recurring;
config.timezones = parsed.attrs.timezones;
config.displayedTimezone = parsed.attrs.displayedTimezone;
config.countdown = parsed.attrs.countdown;
addSingleLocalDate(buffer, state, config);
}
function addLocalRange(buffer, matches, state) {
let config = defaultDateConfig();
let date, time;
const parsed = parseTagAttributes(matches[1]);
config.format = parsed.attrs.format;
config.calendar = parsed.attrs.calendar;
config.timezone = (parsed.attrs.timezone || "").trim();
config.recurring = parsed.attrs.recurring;
config.timezones = parsed.attrs.timezones;
config.displayedTimezone = parsed.attrs.displayedTimezone;
config.countdown = parsed.attrs.countdown;
config.range = parsed.attrs.from && parsed.attrs.to;
if (parsed.attrs.from) {
[date, time] = parsed.attrs.from.split("T");
config.date = date;
config.time = time;
addSingleLocalDate(buffer, state, config);
}
if (config.range) {
closeBuffer(buffer, state, "→");
}
if (parsed.attrs.to) {
[date, time] = parsed.attrs.to.split("T");
config.date = date;
config.time = time;
addSingleLocalDate(buffer, state, config);
}
}
function closeBuffer(buffer, state, text) { function closeBuffer(buffer, state, text) {
let token; let token;
@ -171,4 +215,13 @@ export function setup(helper) {
md.core.textPostProcess.ruler.push("discourse-local-dates", rule); md.core.textPostProcess.ruler.push("discourse-local-dates", rule);
}); });
helper.registerPlugin((md) => {
const rule = {
matcher: /\[date-range(.+?)\]/,
onMatch: addLocalRange,
};
md.core.textPostProcess.ruler.push("discourse-local-dates", rule);
});
} }

View File

@ -86,4 +86,34 @@ RSpec.describe "Local Dates" do
expect(cooked).to include("data-countdown=") expect(cooked).to include("data-countdown=")
end end
context 'ranges' do
it 'generates ranges without time' do
raw = "[date-range from=2022-01-06 to=2022-01-08]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2022-01-06')
expect(cooked).to include('data-range="true"')
expect(cooked).not_to include('data-time=')
end
it 'supports time and timezone' do
raw = "[date-range from=2022-01-06T13:00 to=2022-01-08 timezone=Australia/Sydney]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2022-01-06')
expect(cooked).to include('data-range="true"')
expect(cooked).to include('data-time="13:00"')
expect(cooked).to include('data-timezone="Australia/Sydney"')
end
it 'generates single date when range without end date' do
raw = "[date-range from=2022-01-06T13:00]"
cooked = Fabricate(:post, raw: raw).cooked
expect(cooked).to include('data-date="2022-01-06')
expect(cooked).to include('data-time="13:00"')
expect(cooked).not_to include('data-range=')
end
end
end end