Make WordPress Core

Opened 4 months ago

Closed 3 months ago

#65482 closed enhancement (fixed)

Strip inline note markers from rendered block output

Reported by: adamsilverstein Owned by: adamsilverstein
Priority: normal Milestone: 7.1
Component: Editor Version:
Severity: normal Keywords: has-patch has-unit-tests commit
Cc: Focuses:

Description

Summary

Block comments (Notes) shipped in WordPress 6.9. The next milestone adds inline (partial-text) Notes — notes anchored to a text selection within a block rather than to the whole block.

An inline note's anchor is an in-content marker:

<mark class="wp-note" data-id="N">…marked text…</mark>

The data-id identifies the note, and because the marker lives in the content its position follows edits — no separate selection offset metadata has to be stored. The marker is kept in the raw post_content (and the REST raw view, revisions, and exports) so the editor can re-attach the note on reload.

The public front end, however, should never expose note metadata. This ticket adds a server-side filter that strips the marker from rendered output only.

Proposed change

Add wp_strip_inline_note_markers(), hooked on render_block, which unwraps each note <mark> and its matching closer while preserving the marked text and any nested formatting.

  • src/wp-includes/comment.php — the new function.
  • src/wp-includes/default-filters.php — registers it on render_block.
  • tests/phpunit/tests/comment/stripInlineNoteMarkers.php — unit coverage.

Why the HTML API instead of a regex

Matching is done with WP_HTML_Tag_Processor::has_class() so only the exact wp-note class token is targeted. This avoids three classes of bug a regex would introduce:

  • An unrelated user or plugin <mark> (a core/text-color highlight, or an unrelated wp-note-foo class) survives byte-for-byte with all attributes intact. A \bwp-note\b word boundary would wrongly match wp-note-foo.
  • A </mark>-looking sequence inside an HTML comment or attribute value is never mistaken for a real closing tag.
  • A nesting stack pairs each note opener with its own closer, so overlapping notes and nested highlight <mark>s resolve correctly.

The HTML API has no public token-removal method yet, so an anonymous WP_HTML_Tag_Processor subclass removes the marker token directly on the parsed token stream.

Testing

Run the unit tests:

phpunit --filter Tests_Comment_StripInlineNoteMarkers

Or manually:

  1. Add an inline note to part of a paragraph in the editor and save.
  2. View the post on the front end.
  3. Confirm the <mark class="wp-note"> wrapper is absent while the marked text remains.
  4. Confirm a non-note <mark> highlight (e.g. from text color) is left untouched.

Change History (8)

This ticket was mentioned in ​PR #12215 on ​WordPress/wordpress-develop by ​@adamsilverstein.


4 months ago
#1

## Summary

Backports the server-side piece of Gutenberg PR ​WordPress/gutenberg#78218, which adds inline (partial-text) Notes - notes anchored to a text selection within a block rather than to the whole block.

An inline note's anchor is an in-content <mark class="wp-note" data-id="N">…</mark> marker. The data-id identifies the note and the marker's position follows edits, so no separate selection metadata is stored. The marker stays in the raw post_content (and the REST raw view, revisions, and exports) so the editor can re-attach the note on reload, but the public front-end HTML should not expose note metadata.

This change strips that marker from rendered output only.

## What changed

  • src/wp-includes/comment.php
    • wp_strip_inline_note_markers() — unwraps each note <mark> and its matching closer on render_block, keeping the marked text and any nested formatting.
  • src/wp-includes/default-filters.php
    • Registers the filter on render_block.
  • tests/phpunit/tests/comment/stripInlineNoteMarkers.php
    • Unit coverage for unwrapping, multi/overlapping/nested markers, pass-through, and the safety cases below.

## Why the HTML API, not a regex

Matching goes through WP_HTML_Tag_Processor::has_class() so only the exact wp-note class token is targeted:

  • An unrelated user or plugin <mark> (a core/text-color highlight, or a wp-note-foo class) survives byte-for-byte with all attributes intact. A \bwp-note\b regex word boundary would wrongly match wp-note-foo.
  • A </mark>-looking sequence inside an HTML comment or attribute value is never mistaken for a real tag.
  • A nesting stack pairs each note opener with its own closer, so overlapping notes and nested highlight <mark>s resolve correctly.

The HTML API has no public token-removal method yet, so an anonymous WP_HTML_Tag_Processor subclass unwraps the marker directly on the parsed token stream.

## Testing instructions

Run the new unit tests:

phpunit --filter Tests_Comment_StripInlineNoteMarkers

Or manually: add an inline note to a paragraph in the editor, view the post on the front end, and confirm the <mark class="wp-note"> wrapper is absent while the text remains, and that any non-note <mark> highlight is untouched.

#2 @dmsnell
4 months ago

@adamsilverstein I guess this isn’t the ticket for it, but did we consider using data-note-id instead of data-id? it might be more resilient against mismatches if we could more concretely identify the MARK elements by this instead of overloading the generic data-id or dataset.id (vs. dataset.noteId)

​@adamsilverstein commented on ​PR #12215:


4 months ago
#3

Thanks for the reviews! Addressed both pieces of feedback in a79faec71d..e605cbf345:

@mukeshpanchal27 - added @ticket 65482 to all of the test methods.

@dmsnell - two follow-ups:

  • Converted the multi-line // comments to /* */ block syntax.
  • On the HTML Processor: I dug into it and ended up keeping WP_HTML_Tag_Processor deliberately. WP_HTML_Processor does guarantee well-formed nesting, but on certain ill-formed nesting - e.g. a note marker crossed with other inline formatting like <mark class="wp-note">a<i>b</mark>c</i> - it sets last_error = 'unsupported' and aborts, which for this filter would leave the note marker (and its data-id metadata) in the rendered output. Since the whole point is to *not* leak note metadata to the front end, abort-on-malformed is the wrong failure mode. Token scanning instead strips every wp-note marker it encounters and degrades gracefully: an unbalanced or stray tag is left exactly as it was rather than corrupting the surrounding markup.

I documented that reasoning in the docblock and added tests for the ill-formed cases (unclosed marker, stray closer, improper nesting) to lock the behavior in.

​@wildworks commented on ​PR #12215:


3 months ago
#4

I would appreciate it if you could proceed with this PR and commit it in time for the 7.1 Beta1 release on July 15.

​@adamsilverstein commented on ​PR #12215:


3 months ago
#5

I would appreciate it if you could proceed with this PR and commit it in time for the 7.1 Beta1 release on July 15.

@t-hamano yes will do, I added it to the php backport list.

#6 @adamsilverstein
3 months ago

  • Keywords commit added
  • Owner set to adamsilverstein
  • Status new → assigned

Approved and ready for commit.

#7 @adamsilverstein
3 months ago

@dmsnell I just saw your feedback above about the data id. Its in a mark tag with a class "wp-note" so I think its ok? happy to reconsider if you feel its worthwhile to change. Open a Gutenberg issue and I will work on it.

#8 @adamsilverstein
3 months ago

  • Resolution → fixed
  • Status assigned → closed

In 62658:

Editor: Strip inline note markers from rendered block output.

Inline (partial-text) notes anchor to a text selection within a block via an in-content <mark class="wp-note" data-id="N"> marker. The marker stays in the raw post_content (and the REST raw view, revisions, and exports) so the editor can re-attach the note on reload, but the public front end should not expose note metadata.

See related Gutenberg pull request: ​https://github.com/WordPress/gutenberg/pull/78218.

Props mukesh27, dmsnell, westonruter.
Fixes #65482.

Note: See TracTickets for help on using tickets.