mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 21:13:36 +00:00
markdown: Use html5 <time> tag for timestamps.
Previously, we had implemented:
<span class="timestamp" data-timestamp="unix time">Original text</span>
The new syntax is:
<time timestamp="ISO 8601 string">Original text</time>
<span class="timestamp-error">Invalid time format: Original text</span>
Since python and JS interpretations of the ISO format are very
slightly different, we force both of them to drop milliseconds
and use 'Z' instead of '+00:00' to represent that the string is
in UTC. The resultant strings look like: 2011-04-11T10:20:30Z.
Fixes #15431.
This commit is contained in:
committed by
Tim Abbott
parent
9423d150ac
commit
6ea3816fa6
@@ -68,7 +68,7 @@ const get_content_element = () => {
|
||||
$content.set_find_results('.user-group-mention', $array([]));
|
||||
$content.set_find_results('a.stream', $array([]));
|
||||
$content.set_find_results('a.stream-topic', $array([]));
|
||||
$content.set_find_results('span.timestamp', $array([]));
|
||||
$content.set_find_results('time', $array([]));
|
||||
$content.set_find_results('.emoji', $array([]));
|
||||
$content.set_find_results('div.spoiler-header', $array([]));
|
||||
return $content;
|
||||
@@ -160,11 +160,10 @@ run_test('timestamp', () => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $timestamp = $.create('timestamp(valid)');
|
||||
$timestamp.attr('data-timestamp', 1);
|
||||
$timestamp.attr('datetime', '1970-01-01T00:00:01Z');
|
||||
const $timestamp_invalid = $.create('timestamp(invalid)');
|
||||
$timestamp.addClass('timestamp');
|
||||
$timestamp_invalid.addClass('timestamp');
|
||||
$content.set_find_results('span.timestamp', $array([$timestamp, $timestamp_invalid]));
|
||||
$timestamp_invalid.attr('datetime', 'invalid');
|
||||
$content.set_find_results('time', $array([$timestamp, $timestamp_invalid]));
|
||||
|
||||
// Initial asserts
|
||||
assert.equal($timestamp.text(), 'never-been-set');
|
||||
@@ -173,12 +172,9 @@ run_test('timestamp', () => {
|
||||
rm.update_elements($content);
|
||||
|
||||
// Final asserts
|
||||
assert($timestamp.hasClass('timestamp'));
|
||||
assert(!$timestamp_invalid.hasClass('timestamp'));
|
||||
assert.equal($timestamp.text(), 'Thu, Jan 1 1970, 12:00 AM');
|
||||
assert.equal($timestamp.attr('title'), "This time is in your timezone. Original text was 'never-been-set'.");
|
||||
assert.equal($timestamp_invalid.text(), 'never-been-set');
|
||||
assert.equal($timestamp_invalid.attr('title'), 'Could not parse timestamp.');
|
||||
assert.equal($timestamp_invalid.text(), 'translated: Could not parse timestamp.');
|
||||
});
|
||||
|
||||
run_test('emoji', () => {
|
||||
|
||||
@@ -300,15 +300,17 @@ function handleTimestamp(time) {
|
||||
// JavaScript dates are in milliseconds, Unix timestamps are in seconds
|
||||
timeobject = moment(time * 1000);
|
||||
}
|
||||
const istimevalid = !(timeobject === null || !timeobject.isValid());
|
||||
|
||||
// Generate HTML
|
||||
let timestring = '<span class="timestamp"';
|
||||
if (istimevalid) {
|
||||
timestring += ' data-timestamp="' + timeobject.unix() + '"';
|
||||
const escaped_time = _.escape(time);
|
||||
if (timeobject === null || !timeobject.isValid()) {
|
||||
// Unsupported time format: rerender accordingly.
|
||||
return `<span class="timestamp-error">Invalid time format: ${escaped_time}</span>`;
|
||||
}
|
||||
timestring += '>' + _.escape(time) + '</span>';
|
||||
return timestring;
|
||||
|
||||
// Use html5 <time> tag for valid timestamps.
|
||||
// render time without milliseconds.
|
||||
const escaped_isotime = _.escape(timeobject.toISOString().split('.')[0] + 'Z');
|
||||
return `<time datetime="${escaped_isotime}">${escaped_time}</time>`;
|
||||
}
|
||||
|
||||
function handleStream(stream_name) {
|
||||
|
||||
@@ -137,19 +137,27 @@ exports.update_elements = (content) => {
|
||||
}
|
||||
});
|
||||
|
||||
content.find('span.timestamp').each(function () {
|
||||
content.find('time').each(function () {
|
||||
// Populate each timestamp span with mentioned time
|
||||
// in user's local timezone.
|
||||
const timestamp = moment.unix($(this).attr('data-timestamp'));
|
||||
if (timestamp.isValid() && $(this).attr('data-timestamp') !== null) {
|
||||
const time_str = $(this).attr('datetime');
|
||||
if (time_str === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Moment throws a large deprecation warning when it has to fallback
|
||||
// to the Date() constructor. We needn't worry here and can let bugdown
|
||||
// handle any dates that moment misses.
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
const timestamp = moment(time_str);
|
||||
if (timestamp.isValid()) {
|
||||
const text = $(this).text();
|
||||
const rendered_time = timerender.render_markdown_timestamp(timestamp,
|
||||
null, text);
|
||||
$(this).text(rendered_time.text);
|
||||
$(this).attr('title', rendered_time.title);
|
||||
} else {
|
||||
$(this).removeClass('timestamp');
|
||||
$(this).attr('title', 'Could not parse timestamp.');
|
||||
$(this).text(i18n.t('Could not parse timestamp.'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
}
|
||||
|
||||
/* Timestamps */
|
||||
.timestamp {
|
||||
time {
|
||||
background: hsl(0, 0%, 93%);
|
||||
border-radius: 3px;
|
||||
padding: 0 0.2em;
|
||||
|
||||
@@ -33,6 +33,7 @@ import ahocorasick
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
import markdown
|
||||
import pytz
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
@@ -1229,23 +1230,34 @@ def possible_avatar_emails(content: str) -> Set[str]:
|
||||
|
||||
class Timestamp(markdown.inlinepatterns.Pattern):
|
||||
def handleMatch(self, match: Match[str]) -> Optional[Element]:
|
||||
span = Element('span')
|
||||
span.set('class', 'timestamp')
|
||||
time_input_string = match.group('time')
|
||||
timestamp = None
|
||||
try:
|
||||
timestamp = dateutil.parser.parse(match.group('time'), tzinfos=get_common_timezones())
|
||||
timestamp = dateutil.parser.parse(time_input_string, tzinfos=get_common_timezones())
|
||||
except ValueError:
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(float(match.group('time')))
|
||||
timestamp = datetime.fromtimestamp(float(time_input_string))
|
||||
except ValueError:
|
||||
pass
|
||||
if timestamp:
|
||||
if timestamp.tzinfo:
|
||||
timestamp = timestamp - timestamp.utcoffset()
|
||||
span.set('data-timestamp', timestamp.strftime("%s"))
|
||||
# Set text to initial input, so even if parsing fails, the data remains intact.
|
||||
span.text = markdown.util.AtomicString(match.group('time'))
|
||||
return span
|
||||
|
||||
if not timestamp:
|
||||
error_element = Element('span')
|
||||
error_element.set('class', 'timestamp-error')
|
||||
error_element.text = markdown.util.AtomicString(
|
||||
f"Invalid time format: {time_input_string}")
|
||||
return error_element
|
||||
|
||||
# Use HTML5 <time> element for valid timestamps.
|
||||
time_element = Element('time')
|
||||
if timestamp.tzinfo:
|
||||
timestamp = timestamp.astimezone(pytz.utc)
|
||||
else:
|
||||
timestamp = pytz.utc.localize(timestamp)
|
||||
time_element.set('datetime', timestamp.isoformat().replace('+00:00', 'Z'))
|
||||
# Set text to initial input, so simple clients translating
|
||||
# HTML to text will at least display something.
|
||||
time_element.text = markdown.util.AtomicString(time_input_string)
|
||||
return time_element
|
||||
|
||||
# All of our emojis(non ZWJ sequences) belong to one of these unicode blocks:
|
||||
# \U0001f100-\U0001f1ff - Enclosed Alphanumeric Supplement
|
||||
|
||||
18
zerver/tests/fixtures/markdown_test_cases.json
vendored
18
zerver/tests/fixtures/markdown_test_cases.json
vendored
@@ -743,34 +743,34 @@
|
||||
{
|
||||
"name": "timestamp_bugdown_only",
|
||||
"input": "!time(Jun 5th 2017, 10:30PM)",
|
||||
"expected_output": "<p><span class=\"timestamp\" data-timestamp=\"1496701800\">Jun 5th 2017, 10:30PM</span></p>",
|
||||
"marked_expected_output": "<p><span class=\"timestamp\">Jun 5th 2017, 10:30PM</span></p>"
|
||||
"expected_output": "<p><time datetime=\"2017-06-05T22:30:00Z\">Jun 5th 2017, 10:30PM</time></p>",
|
||||
"marked_expected_output": "<p><span class=\"timestamp-error\">Invalid time format: Jun 5th 2017, 10:30PM</span></p>"
|
||||
},
|
||||
{
|
||||
"name": "timestamp_bugdown_and_marked",
|
||||
"input": "!time(31 Dec 2017)",
|
||||
"expected_output": "<p><span class=\"timestamp\" data-timestamp=\"1514678400\">31 Dec 2017</span></p>"
|
||||
"expected_output": "<p><time datetime=\"2017-12-31T00:00:00Z\">31 Dec 2017</time></p>"
|
||||
},
|
||||
{
|
||||
"name": "timestamp_invalid_input",
|
||||
"input": "!time(<alert(1)>)",
|
||||
"expected_output": "<p><span class=\"timestamp\"><alert(1</span>>)</p>"
|
||||
"expected_output": "<p><span class=\"timestamp-error\">Invalid time format: <alert(1</span>>)</p>"
|
||||
},
|
||||
{
|
||||
"name": "timestamp_timezone",
|
||||
"input": "!time(31 Dec 2017 5:30 am IST)",
|
||||
"expected_output": "<p><span class=\"timestamp\" data-timestamp=\"1514678400\">31 Dec 2017 5:30 am IST</span></p>",
|
||||
"marked_expected_output": "<p><span class=\"timestamp\">31 Dec 2017 5:30 am IST</span></p>"
|
||||
"expected_output": "<p><time datetime=\"2017-12-31T00:00:00Z\">31 Dec 2017 5:30 am IST</time></p>",
|
||||
"marked_expected_output": "<p><span class=\"timestamp-error\">Invalid time format: 31 Dec 2017 5:30 am IST</span></p>"
|
||||
},
|
||||
{
|
||||
"name": "timestamp_incorrect",
|
||||
"input": "!time(hello world)",
|
||||
"expected_output": "<p><span class=\"timestamp\">hello world</span></p>"
|
||||
"input": "!time(**hello world**)",
|
||||
"expected_output": "<p><span class=\"timestamp-error\">Invalid time format: **hello world**</span></p>"
|
||||
},
|
||||
{
|
||||
"name": "timestamp_unix",
|
||||
"input": "!time(1496701800)",
|
||||
"expected_output": "<p><span class=\"timestamp\" data-timestamp=\"1496701800\">1496701800</span></p>"
|
||||
"expected_output": "<p><time datetime=\"2017-06-05T22:30:00Z\">1496701800</time></p>"
|
||||
},
|
||||
{
|
||||
"name": "tex_inline",
|
||||
|
||||
Reference in New Issue
Block a user