Repository navigation
Accept silent spaces and bar separators, and report unknown commands - #32
Merged
Merged
Conversation
Three fixes in the grammar and the rule handlers, all reported from the docs
audit that came out of the example rewrite.
A standalone _ is a silent space. The Space -> UNDER_SCORE production has been
in the grammar from the start with no token declared behind it, so _ lexed as
an ordinary identifier character and rendered as visible text. The token is
guarded by a lookahead, /_(?!{IdentChar})/, so only an underscore that stands
alone becomes a space. ___ stays a literal, as does an underscore inside a
word, which keeps anyone writing ___ as a label working. Write _ _ _ for three
silent spaces.
| and || in a role line are skipped. beatviews.ts already draws a line at the
end of each bar and a double line at the end of the avartanam from the cycle,
so a typed bar was always redundant with what gets drawn. Skipping means a
misplaced bar is not an error either, which is the cost of the one-line
version; checking a bar against the cycle needs the layout pass and is a
feature rather than a grammar fix.
An unknown command is recorded as a ParseError instead of thrown.
createCommand runs inside a rule handler, so its throw escaped parse and load
entirely and reached the caller as an exception while every other parse
failure arrived in the errors array. load() had two failure shapes and its
signature advertised one. The command and any block attached to it are
dropped, and parsing carries on.
One existing assertion changes. docs/tests/playground-patterns.spec.ts
asserted .toThrow("Invalid command"), which pinned the old behaviour, and now
asserts the error is reported. 20 assertions added, 1 replaced, none weakened.
Fixes #17, fixes #19, fixes #28
panyam
commented
Sep 21, 2026
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.
What changes
A standalone
_is a silent space.|and||inside a role line are ignored rather than rejected. An unknown command is reported through the errors array instead of thrown out ofload().All three came out of the docs audit. Three of the seven constructs in issue 27 are now answered, and the two open questions that issue raised about underscores turned out to be already settled in
src/utils.ts.Fixes #17, fixes #19, fixes #28.
Prerequisite knowledge
_as a silent space the whole time. This PR makes that true and adds a Bar Separators section next to Whitespace.src/utils.ts:107,parseSyllable, which is where the project already wrote down what_means. It buildsnew Space(ONE, true)for_andnew Space(ONE, false)for,, one unit each.Reviewer's guide
A theatre programme can list a scene the company never staged, and nobody notices until someone turns up expecting to watch it. The grammar here has had
Space -> UNDER_SCORE { newSilentSpace }in it from the beginning, and the handler behind it builds a perfectly good silent space, but no%token UNDER_SCOREwas ever declared. The production sat there unreachable,_fell through to the identifier rule, and a silent space rendered as the literal text_. So the interesting part of this change is not writing new behaviour, it is connecting behaviour that was already written and already documented. The same shape shows up twice more. Bar separators are drawn by the renderer from the cycle and were rejected by the tokenizer when typed, andcreateCommandalready knew how to describe an unknown command and threw the description instead of filing it.Read in this order:
UNDER_SCOREtoken is the scene that was in the programme. Check the lookahead hardest, since it is what keeps the change non-breaking.How it works
_is itself anIdentChar, so the lookahead is what separates a standalone underscore from a run or a word. At the head of___, the lookahead fails,IDENTmatches all three characters and the run stays a literal. At the head of_ R, the lookahead succeeds and the token wins. tlex resolves these by declaration order at equal priority, which is whyUNDER_SCOREsits aboveIDENTrather than below it.The skip rule cannot reach inside
\cycle("|4|2|2|"), because the tokenizer matches the whole quoted run as oneSTRINGat the opening quote and never offers the interior to another rule. There is a test asserting the cycle spec survives, since that is the one way this change could quietly break a document that parses.For unknown commands:
endBlocksplices a block's commands out ofthis.commandsbeforenewCommandWithBlockruns, so returning null there drops the block rather than leaving its contents stranded at the top level. There is a test for that.Decision log
/_/plain. Plain/_/also works and reads more simply, and it turns___into three silent spaces, which would change the meaning of any document using___as a label. One_per silent space is whatparseSyllablealready does and what the syntax reference table already said, so_ _ _is the spelling and the lookahead keeps runs out of it.IdentCharalone. Excluding_from it would make the grammar agree withparseSyllable, which splitsS_N_Dinto five atoms, but it would also break every literal anyone has written with an underscore in it. The divergence is worth its own issue rather than a silent change here.___and|to the docs examples. They were rewritten in Make every docs example parse, and add a test that keeps it that way #29 to work around exactly these two gaps, and putting them back is a separate change that this PR does not need in order to be correct.Risk / blast radius
parse()andload(). The grammar edits are additive, and the guards below show the three inputs that could have changed meaning and do not.createCommandis public and its return type widens toCommand | null. A caller calling it directly and not checking would now get a null where it used to get an exception. It has a doc comment stating the contract.Silent Space TestsandBar Separator Testsinsrc/tests/parser.spec.ts,Invalid Commandsinsrc/tests/regressions.spec.ts. Suite goes from 473 to 489 passing. 20 assertions added, 1 replaced, none weakened.|, and the only reason they do not collide is that the tokenizer never looks inside a matchedSTRING.Bars inside the cycle spec still define the barsis the test that would catch it, and the before/after below shows[4,2,2]intact._being anIdentCharis the whole reason it is needed, and ifIdentCharever changes, this token's behaviour changes with it.parser.ts, touches the same file's imports. Issue 30 is unaffected.Before / after
Captured by building
lib/cjsonmaster, running the script, then again on this branch.Rows two and three are the point of the lookahead. They are identical in both columns, and there are tests pinning them that pass on master as well as here.
The cycle spec, which is the thing the skip rule had to leave alone:
Out of scope
parseSyllable("S_N_D"), which splits into five atoms, and the grammar, which keeps one literal. Worth a follow-up.___and|to the docs examples.