Repository navigation
Data markup: layers that say what text is, kept by WithoutVisualMarkup() #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| using System.Text.Json; | ||
| using MarkupString.Ansi; | ||
| using MarkupString.Html; | ||
| namespace MarkupString.Tests; | ||
|
|
||
| /// <summary> | ||
| /// Data markup says what text is, not how it looks: no format draws it, taking the visual markup off keeps | ||
| /// it, and it is carried and stored like any other layer. | ||
| /// </summary> | ||
| public class DataMarkupTests | ||
| { | ||
| private sealed record Separator : IDataMarkup; | ||
|
|
||
| private sealed class SeparatorCodec : IMarkupCodec | ||
| { | ||
| public string Kind => "test-separator"; | ||
| public Type MarkupType => typeof(Separator); | ||
| public void Write(Utf8JsonWriter writer, IMarkup markup) { } | ||
| public IMarkup Read(JsonElement element) => new Separator(); | ||
| } | ||
|
|
||
| /// <summary>Draws a separator as <c>{|}</c>, to show that the renderer never calls an emitter for data.</summary> | ||
| private sealed class SeparatorEmitter : IMarkupEmitter | ||
| { | ||
| public Type MarkupType => typeof(Separator); | ||
| public MarkupFormat Format => MarkupFormat.Ansi; | ||
| public void Emit(IMarkup markup, ReadOnlySpan<char> body, in EmitContext context, System.Buffers.IBufferWriter<char> output) | ||
| { | ||
| output.Write("{"); | ||
| output.Write(body); | ||
| output.Write("}"); | ||
| } | ||
| } | ||
|
|
||
| private static readonly MarkupRegistry Registry = MarkupRegistry.Empty.WithAnsi().WithHtml().With(new SeparatorCodec()); | ||
|
|
||
| private static readonly AnsiMarkup Red = AnsiMarkup.Create(foreground: new AnsiColor.Standard(1, false)); | ||
|
|
||
| private static MarkupText Marked(string text) => MarkupText.Wrap(new Separator(), text); | ||
|
|
||
| private static bool IsData(MarkupText text, int index) => | ||
| text.Runs.Any(run => index >= run.Start && index < run.End && run.Markups.Any(markup => markup is IDataMarkup)); | ||
|
|
||
| [Test] | ||
| public async Task DataIsWrittenAsItsText() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Plain("a"), Marked("|"), MarkupText.Plain("b")]); | ||
|
|
||
| await Assert.That(text.Render(MarkupFormat.Ansi, Registry)).IsEqualTo("a|b"); | ||
| await Assert.That(text.Render(MarkupFormat.Html, Registry)).IsEqualTo("a|b"); | ||
| await Assert.That(text.Render(MarkupFormat.Plain, Registry)).IsEqualTo("a|b"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task DataUnderColourIsWrittenInTheColour() | ||
| { | ||
| var coloured = MarkupText.Concat([MarkupText.Plain("a"), MarkupText.Wrap(Red, Marked("|")), MarkupText.Plain("b")]); | ||
| var colourOnly = MarkupText.Concat([MarkupText.Plain("a"), MarkupText.Wrap(Red, "|"), MarkupText.Plain("b")]); | ||
|
|
||
| await Assert.That(coloured.Render(MarkupFormat.Ansi, Registry)).IsEqualTo(colourOnly.Render(MarkupFormat.Ansi, Registry)); | ||
| await Assert.That(coloured.Render(MarkupFormat.Html, Registry)).IsEqualTo(colourOnly.Render(MarkupFormat.Html, Registry)); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task AnEmitterForDataIsNeverCalled() | ||
| { | ||
| var registry = Registry.With(new SeparatorEmitter()); | ||
|
|
||
| await Assert.That(MarkupText.Concat([MarkupText.Plain("a"), Marked("|")]).Render(MarkupFormat.Ansi, registry)).IsEqualTo("a|"); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutVisualMarkup_KeepsTheDataAndDropsTheColour() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Wrap(Red, "a"), MarkupText.Wrap(Red, Marked("|")), MarkupText.Plain("b"), Marked("|")]); | ||
|
|
||
| var stripped = text.WithoutVisualMarkup(); | ||
|
|
||
| await Assert.That(stripped.Text).IsEqualTo("a|b|"); | ||
| await Assert.That(stripped.Runs.Length).IsEqualTo(2); | ||
| await Assert.That(stripped.Runs.All(run => run.Markups.Count == 1 && run.Markups[0] is Separator)).IsTrue(); | ||
| await Assert.That(IsData(stripped, 1)).IsTrue(); | ||
| await Assert.That(IsData(stripped, 3)).IsTrue(); | ||
| await Assert.That(IsData(stripped, 0)).IsFalse(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutVisualMarkup_JoinsDataThatColourHadSplit() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Wrap(Red, Marked("|")), Marked("|")]); | ||
|
|
||
| var stripped = text.WithoutVisualMarkup(); | ||
|
|
||
| await Assert.That(stripped.Runs.Length).IsEqualTo(1); | ||
| await Assert.That(stripped.Runs[0].Length).IsEqualTo(2); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutVisualMarkup_WithNoData_IsThePlainText() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Wrap(Red, "a"), MarkupText.Bell(), MarkupText.Plain("b")]); | ||
|
|
||
| var stripped = text.WithoutVisualMarkup(); | ||
|
|
||
| await Assert.That(stripped.Text).IsEqualTo("ab"); | ||
| await Assert.That(stripped.Runs.IsDefaultOrEmpty).IsTrue(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutVisualMarkup_DropsPointsWithTheirCarriers() | ||
| { | ||
| var text = MarkupText.Concat([Marked("|"), MarkupText.Bell(), MarkupText.Plain("b")]); | ||
|
|
||
| var stripped = text.WithoutVisualMarkup(); | ||
|
|
||
| await Assert.That(stripped.Text).IsEqualTo("|b"); | ||
| await Assert.That(IsData(stripped, 0)).IsTrue(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutDataMarkup_KeepsTheColourAndDropsTheData() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Wrap(Red, "a"), MarkupText.Wrap(Red, Marked("|")), MarkupText.Plain("b"), Marked("|")]); | ||
|
|
||
| var unmarked = text.WithoutDataMarkup(); | ||
|
|
||
| await Assert.That(unmarked.Text).IsEqualTo("a|b|"); | ||
| await Assert.That(IsData(unmarked, 1)).IsFalse(); | ||
| await Assert.That(IsData(unmarked, 3)).IsFalse(); | ||
| await Assert.That(unmarked.Render(MarkupFormat.Ansi, Registry)) | ||
| .IsEqualTo(MarkupText.Concat([MarkupText.Wrap(Red, "a|"), MarkupText.Plain("b|")]).Render(MarkupFormat.Ansi, Registry)); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutDataMarkup_KeepsPoints() | ||
| { | ||
| var text = MarkupText.Concat([Marked("|"), MarkupText.Bell(), MarkupText.Plain("b")]); | ||
|
|
||
| var unmarked = text.WithoutDataMarkup(); | ||
|
|
||
| await Assert.That(unmarked.Text).IsEqualTo(text.Text); | ||
| await Assert.That(unmarked.ToPlainText()).IsEqualTo("|b"); | ||
| await Assert.That(IsData(unmarked, 0)).IsFalse(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task WithoutDataMarkup_WithNoData_IsTheSameText() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Wrap(Red, "a"), MarkupText.Plain("b")]); | ||
|
|
||
| await Assert.That(ReferenceEquals(text.WithoutDataMarkup(), text)).IsTrue(); | ||
| } | ||
|
|
||
| [Test] | ||
| public async Task DataIsIgnoredByEquality() | ||
| => await Assert.That(MarkupText.Concat([MarkupText.Plain("a"), Marked("|")])).IsEqualTo(MarkupText.Plain("a|")); | ||
|
|
||
| [Test] | ||
| public async Task DataIsStoredAndReadBack() | ||
| { | ||
| var text = MarkupText.Concat([MarkupText.Plain("a"), Marked("|"), MarkupText.Plain("b")]); | ||
|
|
||
| var back = MarkupTextSerializer.Deserialize(MarkupTextSerializer.Serialize(text, Registry), Registry); | ||
|
|
||
| await Assert.That(IsData(back, 1)).IsTrue(); | ||
| await Assert.That(IsData(back, 0)).IsFalse(); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace MarkupString; | ||
|
|
||
| /// <summary> | ||
| /// A markup that says what the text it covers is, rather than how it looks: that a character is a list | ||
| /// separator, say, and not part of an item. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para>No format writes anything for a data layer. The renderer leaves it out before any emitter sees the | ||
| /// run, so the text it covers is written as the layers around it would write it, and an emitter registered | ||
| /// for a data type is never called.</para> | ||
| /// <para>Data stays where visual markup goes: <see cref="MarkupText.WithoutVisualMarkup"/> takes every other | ||
| /// layer off and keeps these. Like every layer, it is ignored by <see cref="MarkupText.ToPlainText"/> and | ||
| /// equality, carried through slicing, concatenation and joining, and stored through its codec | ||
| /// (<see cref="IMarkupCodec"/>); a reader without the codec keeps it as an <see cref="UnknownMarkup"/>.</para> | ||
| /// </remarks> | ||
| public interface IDataMarkup : IMarkup; |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| #nullable enable | ||
| MarkupString.TextEncoding.Markdown = 4 -> MarkupString.TextEncoding | ||
| static readonly MarkupString.MarkupFormat.Markdown -> MarkupString.MarkupFormat! | ||
| MarkupString.IDataMarkup | ||
| MarkupString.MarkupText.WithoutVisualMarkup() -> MarkupString.MarkupText! | ||
| MarkupString.MarkupText.WithoutDataMarkup() -> MarkupString.MarkupText! |
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
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Filter data before emitter selection and context construction.
For a run with data and visual layers,
EmitContext.Markupsstill contains the data layer, even thoughmarkupsis filtered here. Visual emitters can therefore observe data through the context. A layer that implements bothIDataMarkupandIBlockMarkupcan also reachTryRenderBlockbefore this filter. Filter the run and adjacent context sets before dispatch, and exclude data layers from block selection.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving this as is for now. The PR is on hold while we decide whether the engine still needs data markup at all. If the PR goes ahead, the filter moves ahead of emitter selection and block dispatch in its next code push.
Generated by Claude Code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HarryCordewenerUnderstood. The finding remains open while the PR is on hold. No action is needed now.You are interacting with an AI system.