Add colour and underline buttons to the Markdown editor - #34
Merged
Merged
Conversation
Markdown has neither, and the website keeps both as
<span class="text-…"> from a fixed list it renders
(phpbb-website-private#19). This gives authors buttons for them:
* U (Ctrl/Cmd-U) underlines the selection, or takes the underline off
again.
* A opens a menu with one entry per palette colour, each showing its
colour, plus "Remove colour". A new colour replaces the old one
rather than nesting.
The list comes from the textarea's data-markdown-styles attribute, which
the website fills from the same constants its renderer checks. Without
it the buttons are left out.
The selection is wrapped one line at a time, with list, heading and quote
markers kept outside the span, because the site only renders a span that
opens and closes within one block. Selecting just the styled words is
enough to change or remove their styling: tags right around the
selection are taken in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of the colour and underline buttons found edits that would put
HTML text or broken layout on published pages. The buttons now:
* Style every selection, not just the primary one.
* Leave lines alone that a span would break: code blocks (from
CodeMirror's own tokens, indented code included), code fences,
rules, setext underlines and table rows.
* Never cut into inline code, emphasis, links or another span. A
selection that would falls back to the whole line's content, or
leaves the line alone if even that would. A selection just inside
emphasis or code markers takes the markers in.
* Start after list markers even when the selection starts inside
one, and keep a hard-break backslash outside the span.
* Take the underline off wherever it is in the selection, also under
a colour.
* Clean whole lines when removing a colour from a selection that
holds half of a pair.
* Bind Cmd/Ctrl-U only when the underline button exists.
The menu labels are no longer bold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second review still found edits the site could not render: two
selections on one line tripping over each other, single-underscore
emphasis and double-backtick code slipping past the balance check,
tables without a leading pipe, reference definitions, a heading's
closing hashes, image alt text, and empty pairs dropped into list
markers, link URLs or between bold markers. The selection was also lost
after every click, so a second button acted on nothing.
Instead of more rules about Markdown, each edit is now tried first with
EasyMDE's own renderer. The line is rendered with a probe span and
without it. The edit is made only if the probe comes out as an element
directly inside its block, the site's rule too, and taking it out again
gives back exactly the rendering without it. If the selected part of a
line fails, the line's whole text is tried once; if that fails too, the
line is left as it is. DOMParser renders into a detached document, so
nothing runs and nothing loads.
Also:
* Selections are handled per line, rightmost part first, so parts of
several selections on one line cannot shift each other.
* Bookmarks keep each selection around the styled text afterwards, so
red then underline, or red then blue, act on the same words.
* Any line with an unescaped pipe outside code counts as a table row
and is left alone, as are reference definitions.
* A heading's closing hashes stay outside the span.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A third review found edits that still slipped through:
* The probe check read the browser's repaired DOM, so marked's badly
nested output for a selection from before bold to inside it looked
clean. marked's raw HTML is now captured through EasyMDE's
sanitizerFunction hook, unchanged, and the probe must hold properly
nested inline content with no block in it.
* Lines were rendered alone, missing bold, links and code spans that
cross a line break, and reference links. The whole paragraph is now
rendered, with every reference definition of the document.
* Fences and code inside quotes and list items were not recognised.
Lines inside a fence are now found by counting fences above.
* Replacing a range that held a selection bookmark threw and lost the
selection. Edits are now small insertions and deletions, and a
lost bookmark is skipped.
* Overlapping or touching selections are merged, and reversed ones
keep their direction.
* Removing a style no longer touches code, and removes both halves of
a pair.
* An empty line gets an empty pair as a paragraph of its own. A
reference definition's continuation line, and a body whose span
tags do not pair up, are left alone.
Checked with the reviewer's puppeteer harness and fuzzer against the
site's SafeMarkdown. Ordinary text in the case files comes out right.
What the fuzzer still flags is delimiter soup where marked and
CommonMark disagree about emphasis; the site now drops rather than
shows any span it cannot restore.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From review, and from fuzzing against the site's renderer:
* Code spans are found by CommonMark's own rule, the site's: a
backtick run is closed by the next run of the same length. A span
edge may not fall inside one, split a run of backticks, follow a
backslash (which would escape the tag), or cross into or out of a
link's or image's brackets. marked alone disagreed with the site on
such odd input.
* Fences are tracked by character and length, so a fence of one kind
inside a block of another no longer confuses the rest of the page.
* An indented paragraph is rendered together with the list item it
continues, so it can be styled.
* A blank line between list items or quote lines gets no empty pair:
a paragraph there would split the list or quote.
* A line that is only inline code is not taken for a code block.
About 34,000 random edits with the reviewer's fuzzer, rendered through
the site's SafeMarkdown, left no span tag showing as text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The JavaScript contains a critical sanitizer issue and several moderate Markdown-editing issues that must be addressed.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds optional colour and underline controls to the EasyMDE Markdown editor, with Markdown validation and new toolbar styling.
Changes:
- Adds data-driven colour and underline editing.
- Adds selection handling, shortcuts, and rendering validation.
- Styles the new toolbar controls and colour menu.
File summaries
| File | Review findings |
|---|---|
js/markdown-editor.js |
Critical, 1 vote (line 98): The sanitizer hook handling prevents probe detection and causes edits to be rejected. Moderate, 1 vote (lines 413, 419, 433, 543, 741, 996): Preserve default shortcuts; avoid treating escaped/code text as style spans; handle even backslash runs in tables; detect headings after list/quote prefixes; correctly identify reference continuations; and expand containing spans for partial selections. |
css/markdown-editor.css |
No final review comments. |
Review details
Suppressed comments (6)
js/markdown-editor.js:996
- EasyMDE treats
shortcutsas the complete shortcut map, so this replaces its default bindings: editors without styles get{}, and styled editors retain onlyunderline. That disables the existing Cmd/Ctrl-B, Cmd/Ctrl-I, Cmd/Ctrl-K and other built-in keyboard shortcuts on every editor. Preserve the default map and add the underline binding instead.
shortcuts: styles && styles.underline ? { underline: 'Cmd-U' } : {},
js/markdown-editor.js:547
spanPairsparses raw Markdown rather than rendered HTML, so tag-shaped text inside an inline code span or escaped HTML is treated as an existing style. Recolouring such a selection removes those literal<span ...>and</span>characters beforerendersCleanlyruns; the probe then compares the already-mutated body and accepts the edit, silently changing the code/text. Only remove pairs that are actual rendered style spans, excluding code and escaped text.
cuts.slice().sort(function (a, b) {
return b[0] - a[0];
}).forEach(function (cut) {
body = body.slice(0, cut[0] - start) + body.slice(cut[0] - start + cut[1]);
});
js/markdown-editor.js:420
- This table check treats a pipe as escaped whenever the immediately preceding character is a backslash. In CommonMark an even run is not an escape, so a row such as
a\\|bcan be parsed as a table while this code styles it, contrary to the stated rule that table rows remain untouched. Count consecutive backslashes modulo two.
if (/(^|[^\\])\|/.test(text.replace(/(`+)[^`]*?\1/g, ''))) {
return true;
js/markdown-editor.js:439
- The closing-hash check only recognizes headings that start directly with
#. For> # Heading #or- # Heading #,BLOCK_PREFIXremoves the quote/list marker but this check fails, so a whole-line selection includes the closing hashes in the probe; the render equality check then rejects an otherwise valid edit. Detect an ATX heading after optional list/quote prefixes so the closing hashes remain outside the span.
if (/^ {0,3}#{1,6}(\s|$)/.test(text)) {
var closing = text.match(/\s+#+\s*$/);
if (closing) {
end = closing.index;
}
}
js/markdown-editor.js:415
- This treats every nonblank line immediately after a reference definition as a reference continuation. A valid document such as
[id]: /urlfollowed directly byParagraphmakes that paragraph ordinary Markdown, but this condition marks it untouchable so it can never be styled or unstyled. Restrict the check to a syntactically valid continuation title (or parse the definition and continuation together).
if (line > 0 && REFERENCE.test(cm.getLine(line - 1)) && !isBlank(text)) {
return true;
}
js/markdown-editor.js:750
- This only expands a selection when the opening and closing tags are immediately adjacent to its edges. Selecting a proper subset of text inside an existing colour span therefore leaves the selection inside the old span: recolouring nests the new span instead of replacing the old one, and “Remove colour” finds no tag overlapping the selection and does nothing. Expand/split containing target spans before applying the edit so partial selections have the same replace/remove behavior as selections restored by the toolbar.
for (;;) {
var opening = before.match(/<span class="[^"<>]*">$/);
var closing = after.match(/^<\/span>/);
if (!opening || !closing) {
break;
}
from = { line: from.line, ch: from.ch - opening[0].length };
to = { line: to.line, ch: to.ch + closing[0].length };
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var html = editor.markdown(markdown); | ||
|
|
||
| return { | ||
| raw: editor.markdownCapture.raw, |
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Colour and underline buttons for the Markdown editor, so new knowledge base articles can use the styling migrated articles keep (issue #19 in paul999/phpbb-website-private). Follows #33, which added the CSS classes.
What authors get
data-markdown-styleson the textarea, fromMarkdownEditorType'stext_stylesoption). The list of classes comes from the website, from the same constants its renderer checks, so this script keeps no copy of the palette. Team blurbs don't get the buttons, because the team page doesn't style these classes.Why the JS is bigger than two buttons
The website keeps colour and underline as
<span class="text-…">.SafeMarkdownrenders such a span only when it pairs up within one block without crossing other formatting, and leaves it off the page otherwise (phpbb/phpbb-website-private#222). So the buttons try every edit before making it:sanitizerFunctionhook, unchanged, so the preview is unaffected. The edit is made only if:Testing
SafeMarkdown:****]***) where marked and CommonMark disagree about emphasis. On the site the worst outcome there is a dropped colour.After this merges,
composer update phpbb/website-assetsgoes into phpbb/phpbb-website-private#222.🤖 Generated with Claude Code