|
| 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) |
0 commit comments