forked from zulip/zulip-flutter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
175cf46
commit 12a1030
Showing
2 changed files
with
124 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import 'dart:math'; | ||
|
||
// | ||
// Put functions for nontrivial message-content generation in this file. | ||
// | ||
// If it's complicated enough to need tests, it should go in here. | ||
// | ||
|
||
// https://spec.commonmark.org/0.30/#fenced-code-blocks | ||
final RegExp _openingBacktickFenceRegex = (() { | ||
// Allow "up to three spaces of indentation". | ||
// As of Zulip Server 7.0, we don't comply with that detail: | ||
// https://chat.zulip.org/#narrow/stream/6-frontend/topic/quote-and-reply.20fence.20length/near/1588273 | ||
// and that's a bug. | ||
const lineStart = r'^ {0,3}'; | ||
|
||
// The backticks, captured so we can see how many. | ||
const backticks = r'(`{3,})'; | ||
|
||
// The "info string" plus (meaningless) leading or trailing spaces or tabs. | ||
// It can't contain backticks. | ||
const trailing = r'[^`]*'; | ||
return RegExp(lineStart + backticks + trailing, multiLine: true); | ||
})(); | ||
|
||
/// The shortest opening backtick fence that's longer than any in [content]. | ||
/// | ||
/// Expressed as a number of backticks. | ||
/// | ||
/// Use this for quote-and-reply or anything else that requires wrapping | ||
/// Markdown in a backtick fence. | ||
/// | ||
/// See the CommonMark spec, which Zulip servers should but don't always follow: | ||
/// https://spec.commonmark.org/0.30/#fenced-code-blocks | ||
int getUnusedOpeningBacktickFenceLength(String content) { | ||
final matches = _openingBacktickFenceRegex.allMatches(content); | ||
int result = 3; | ||
for (final match in matches) { | ||
result = max(result, match[1]!.length); | ||
} | ||
return result; | ||
} | ||
|
||
/// Wrap Markdown [content] with opening and closing backtick fences. | ||
/// | ||
/// For example: | ||
/// | ||
/// const str = | ||
/// '```javascript\n' | ||
/// 'console.log(\'Hello world!\');\n' | ||
/// '```'; | ||
/// print(wrapWithBacktickFence(content, 'quote')); | ||
/// // Output: | ||
/// // | ||
/// // ````quote | ||
/// // ```javascript | ||
/// // console.log('Hello world!'); | ||
/// // ``` | ||
/// // ```` | ||
/// | ||
/// This does not parse [content] to make sure its backtick fences are properly | ||
/// paired and nested. If they aren't, it's probably not clear what | ||
/// backtick-fenced output would be more reasonable anyway -- | ||
/// especially for callers like quote-and-reply that use this string as a | ||
/// best-effort suggestion for the user to inspect and fix before sending. | ||
/// (Render previews, #178, will help the user with that.) | ||
/// | ||
/// In [content], indented code blocks | ||
/// ( https://spec.commonmark.org/0.30/#indented-code-blocks ) | ||
/// and code blocks fenced with tildes should make no difference to the | ||
/// backtick fences we choose here; this function ignores them. | ||
/// | ||
/// See the CommonMark spec, which Zulip servers should but don't always follow: | ||
/// https://spec.commonmark.org/0.30/#fenced-code-blocks | ||
// TODO(#178) Remove mention of #178 in doc. | ||
String wrapWithBacktickFence({required String content, String? infoString}) { | ||
assert(infoString == null || !infoString.contains('`')); | ||
assert(infoString == null || infoString.trim() == infoString); | ||
|
||
StringBuffer resultBuffer = StringBuffer(); | ||
|
||
// (A) Why not specially handle dangling opening fences | ||
// (ones without a corresponding closing fence)? | ||
// Because the spec allows leaving the closing fence implicit: | ||
// > If the end of the containing block (or document) is reached | ||
// > and no closing code fence has been found, | ||
// > the code block contains all of the lines after the opening code fence | ||
// > until the end of the containing block (or document). | ||
// | ||
// (B) Why not look for dangling closing fences (ones without an opening fence)? | ||
// Because technically there's no such thing: | ||
// they would be indistinguishable from dangling opening fences, | ||
// and parsers will treat them that way. (See A for what that treatment is.) | ||
final fenceLength = getUnusedOpeningBacktickFenceLength(content); | ||
|
||
for (int i = 0; i < fenceLength; i++) { | ||
resultBuffer.write('`'); | ||
} | ||
if (infoString != null) { | ||
resultBuffer.write(infoString); | ||
} | ||
resultBuffer.write('\n'); | ||
resultBuffer.write(content); | ||
resultBuffer.write('\n'); | ||
for (int i = 0; i < fenceLength; i++) { | ||
resultBuffer.write('`'); | ||
} | ||
return resultBuffer.toString(); | ||
} | ||
|
||
// TODO more, like /near links to messages in conversations | ||
// (also to be used in quote-and-reply) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'package:checks/checks.dart'; | ||
import 'package:test/scaffolding.dart'; | ||
import 'package:zulip/model/compose.dart'; | ||
|
||
void main() { | ||
group('wrapWithBacktickFence', () { | ||
// TODO: Find a nice compact way to write test cases. :-) The tricky bit is | ||
// that almost all of them will involve two multiline strings that need to | ||
// be easy to read (input and expected output). Can we e.g. read data from | ||
// a separate file in a format that's convenient to maintain? | ||
}); | ||
} |