Skip to content

Commit 12a1030

Browse files
committed
WIP model: Add wrapWithBacktickFence, to use with quote-and-reply
TODO tests. For the corresponding logic used in the web app and zulip-mobile, see web/shared/src/fenced_code.ts. I believe the logic here differs from that in just this way: it follows the CommonMark spec more closely by disqualifying backtick-fence lines where the "info string" has a backtick, since that's not allowed: > If the info string comes after a backtick fence, it may not > contain any backtick characters. (The reason for this restriction > is that otherwise some inline code would be incorrectly > interpreted as the beginning of a fenced code block.) Regarding the new file lib/model/compose.dart, we do have existing code that could reasonably move here, but it's pretty simple. It's the code that gives the upload-file Markdown; see registerUploadStart and registerUploadEnd in [ContentTextEditingController]. Related: zulip#116
1 parent 175cf46 commit 12a1030

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

lib/model/compose.dart

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import 'dart:math';
2+
3+
//
4+
// Put functions for nontrivial message-content generation in this file.
5+
//
6+
// If it's complicated enough to need tests, it should go in here.
7+
//
8+
9+
// https://spec.commonmark.org/0.30/#fenced-code-blocks
10+
final RegExp _openingBacktickFenceRegex = (() {
11+
// Allow "up to three spaces of indentation".
12+
// As of Zulip Server 7.0, we don't comply with that detail:
13+
// https://chat.zulip.org/#narrow/stream/6-frontend/topic/quote-and-reply.20fence.20length/near/1588273
14+
// and that's a bug.
15+
const lineStart = r'^ {0,3}';
16+
17+
// The backticks, captured so we can see how many.
18+
const backticks = r'(`{3,})';
19+
20+
// The "info string" plus (meaningless) leading or trailing spaces or tabs.
21+
// It can't contain backticks.
22+
const trailing = r'[^`]*';
23+
return RegExp(lineStart + backticks + trailing, multiLine: true);
24+
})();
25+
26+
/// The shortest opening backtick fence that's longer than any in [content].
27+
///
28+
/// Expressed as a number of backticks.
29+
///
30+
/// Use this for quote-and-reply or anything else that requires wrapping
31+
/// Markdown in a backtick fence.
32+
///
33+
/// See the CommonMark spec, which Zulip servers should but don't always follow:
34+
/// https://spec.commonmark.org/0.30/#fenced-code-blocks
35+
int getUnusedOpeningBacktickFenceLength(String content) {
36+
final matches = _openingBacktickFenceRegex.allMatches(content);
37+
int result = 3;
38+
for (final match in matches) {
39+
result = max(result, match[1]!.length);
40+
}
41+
return result;
42+
}
43+
44+
/// Wrap Markdown [content] with opening and closing backtick fences.
45+
///
46+
/// For example:
47+
///
48+
/// const str =
49+
/// '```javascript\n'
50+
/// 'console.log(\'Hello world!\');\n'
51+
/// '```';
52+
/// print(wrapWithBacktickFence(content, 'quote'));
53+
/// // Output:
54+
/// //
55+
/// // ````quote
56+
/// // ```javascript
57+
/// // console.log('Hello world!');
58+
/// // ```
59+
/// // ````
60+
///
61+
/// This does not parse [content] to make sure its backtick fences are properly
62+
/// paired and nested. If they aren't, it's probably not clear what
63+
/// backtick-fenced output would be more reasonable anyway --
64+
/// especially for callers like quote-and-reply that use this string as a
65+
/// best-effort suggestion for the user to inspect and fix before sending.
66+
/// (Render previews, #178, will help the user with that.)
67+
///
68+
/// In [content], indented code blocks
69+
/// ( https://spec.commonmark.org/0.30/#indented-code-blocks )
70+
/// and code blocks fenced with tildes should make no difference to the
71+
/// backtick fences we choose here; this function ignores them.
72+
///
73+
/// See the CommonMark spec, which Zulip servers should but don't always follow:
74+
/// https://spec.commonmark.org/0.30/#fenced-code-blocks
75+
// TODO(#178) Remove mention of #178 in doc.
76+
String wrapWithBacktickFence({required String content, String? infoString}) {
77+
assert(infoString == null || !infoString.contains('`'));
78+
assert(infoString == null || infoString.trim() == infoString);
79+
80+
StringBuffer resultBuffer = StringBuffer();
81+
82+
// (A) Why not specially handle dangling opening fences
83+
// (ones without a corresponding closing fence)?
84+
// Because the spec allows leaving the closing fence implicit:
85+
// > If the end of the containing block (or document) is reached
86+
// > and no closing code fence has been found,
87+
// > the code block contains all of the lines after the opening code fence
88+
// > until the end of the containing block (or document).
89+
//
90+
// (B) Why not look for dangling closing fences (ones without an opening fence)?
91+
// Because technically there's no such thing:
92+
// they would be indistinguishable from dangling opening fences,
93+
// and parsers will treat them that way. (See A for what that treatment is.)
94+
final fenceLength = getUnusedOpeningBacktickFenceLength(content);
95+
96+
for (int i = 0; i < fenceLength; i++) {
97+
resultBuffer.write('`');
98+
}
99+
if (infoString != null) {
100+
resultBuffer.write(infoString);
101+
}
102+
resultBuffer.write('\n');
103+
resultBuffer.write(content);
104+
resultBuffer.write('\n');
105+
for (int i = 0; i < fenceLength; i++) {
106+
resultBuffer.write('`');
107+
}
108+
return resultBuffer.toString();
109+
}
110+
111+
// TODO more, like /near links to messages in conversations
112+
// (also to be used in quote-and-reply)

test/model/compose_test.dart

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:test/scaffolding.dart';
3+
import 'package:zulip/model/compose.dart';
4+
5+
void main() {
6+
group('wrapWithBacktickFence', () {
7+
// TODO: Find a nice compact way to write test cases. :-) The tricky bit is
8+
// that almost all of them will involve two multiline strings that need to
9+
// be easy to read (input and expected output). Can we e.g. read data from
10+
// a separate file in a format that's convenient to maintain?
11+
});
12+
}

0 commit comments

Comments
 (0)