|
| 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 | + // Recognize a fence with "up to three spaces of indentation". |
| 12 | + // Servers don't recognize fences that start with spaces, as of Server 7.0: |
| 13 | + // https://chat.zulip.org/#narrow/stream/6-frontend/topic/quote-and-reply.20fence.20length/near/1588273 |
| 14 | + // but that's a bug, since those fences are valid in the spec. |
| 15 | + // Still, it's harmless to make our own fence longer even if the server |
| 16 | + // wouldn't notice the internal fence that we're steering clear of, |
| 17 | + // and if servers *do* start following the spec by noticing indented internal |
| 18 | + // fences, then this client behavior will be nice. |
| 19 | + const lineStart = r'^ {0,3}'; |
| 20 | + |
| 21 | + // The backticks, captured so we can see how many. |
| 22 | + const backticks = r'(`{3,})'; |
| 23 | + |
| 24 | + // The "info string" plus (meaningless) leading or trailing spaces or tabs. |
| 25 | + // It can't contain backticks. |
| 26 | + const trailing = r'[^`]*$'; |
| 27 | + return RegExp(lineStart + backticks + trailing, multiLine: true); |
| 28 | +})(); |
| 29 | + |
| 30 | +/// The shortest backtick fence that's longer than any in [content]. |
| 31 | +/// |
| 32 | +/// Expressed as a number of backticks. |
| 33 | +/// |
| 34 | +/// Use this for quote-and-reply or anything else that requires wrapping |
| 35 | +/// Markdown in a backtick fence. |
| 36 | +/// |
| 37 | +/// See the CommonMark spec, which Zulip servers should but don't always follow: |
| 38 | +/// https://spec.commonmark.org/0.30/#fenced-code-blocks |
| 39 | +int getUnusedBacktickFenceLength(String content) { |
| 40 | + final matches = _openingBacktickFenceRegex.allMatches(content); |
| 41 | + int result = 3; |
| 42 | + for (final match in matches) { |
| 43 | + result = max(result, match[1]!.length + 1); |
| 44 | + } |
| 45 | + return result; |
| 46 | +} |
| 47 | + |
| 48 | +/// Wrap Markdown [content] with opening and closing backtick fences. |
| 49 | +/// |
| 50 | +/// For example, for this Markdown: |
| 51 | +/// |
| 52 | +/// ```javascript |
| 53 | +/// console.log('Hello world!'); |
| 54 | +/// ``` |
| 55 | +/// |
| 56 | +/// this function, with `infoString: 'quote'`, gives |
| 57 | +/// |
| 58 | +/// ````quote |
| 59 | +/// ```javascript |
| 60 | +/// console.log('Hello world!'); |
| 61 | +/// ``` |
| 62 | +/// ```` |
| 63 | +/// |
| 64 | +/// See the CommonMark spec, which Zulip servers should but don't always follow: |
| 65 | +/// https://spec.commonmark.org/0.30/#fenced-code-blocks |
| 66 | +// In [content], indented code blocks |
| 67 | +// ( https://spec.commonmark.org/0.30/#indented-code-blocks ) |
| 68 | +// and code blocks fenced with tildes should make no difference to the |
| 69 | +// backtick fences we choose here; this function ignores them. |
| 70 | +String wrapWithBacktickFence({required String content, String? infoString}) { |
| 71 | + assert(infoString == null || !infoString.contains('`')); |
| 72 | + assert(infoString == null || infoString.trim() == infoString); |
| 73 | + |
| 74 | + StringBuffer resultBuffer = StringBuffer(); |
| 75 | + |
| 76 | + // CommonMark doesn't require closing fences to be paired: |
| 77 | + // https://github.com/zulip/zulip-flutter/pull/179#discussion_r1228712591 |
| 78 | + // |
| 79 | + // - We need our opening fence to be long enough that it won't be closed by |
| 80 | + // any fence in the content. |
| 81 | + // - We need our closing fence to be long enough that it will close any |
| 82 | + // outstanding opening fences in the content. |
| 83 | + final fenceLength = getUnusedBacktickFenceLength(content); |
| 84 | + |
| 85 | + resultBuffer.write('`' * fenceLength); |
| 86 | + if (infoString != null) { |
| 87 | + resultBuffer.write(infoString); |
| 88 | + } |
| 89 | + resultBuffer.write('\n'); |
| 90 | + resultBuffer.write(content); |
| 91 | + if (content.isNotEmpty && !content.endsWith('\n')) { |
| 92 | + resultBuffer.write('\n'); |
| 93 | + } |
| 94 | + resultBuffer.write('`' * fenceLength); |
| 95 | + resultBuffer.write('\n'); |
| 96 | + return resultBuffer.toString(); |
| 97 | +} |
| 98 | + |
| 99 | +// TODO more, like /near links to messages in conversations |
| 100 | +// (also to be used in quote-and-reply) |
0 commit comments