Strip a leading BOM from source - #33
Conversation
NullVoxPopuli
left a comment
There was a problem hiding this comment.
instead of supporting BOM, let's just also strip it, and return the whole content without the BOM
I'm not sure what software is adding a BOM for you, but it is unneeded
You got it, boss. ❤️ |
|
Should we fail loudly with a “content tag utils does not support content with BOM” or maybe warn the user that it will be stripped? |
|
why does the user need to know / care they have a BOM or not? we can document that we strip the BOM, but we should not log anything |
e9127a2 to
f573ade
Compare
|
Reworked it to strip instead of offset. Came out smaller than what I had, so thanks for the nudge. Description's rewritten too. One thing I'd like your call on though. content-tag strips exactly one BOM, which I think is right: a BOM is only ever the first character, and a second U+FEFF is just a ZWNBSP, so it's content. But that means transformSync("" + source, (c) => c.replace("Hello", "Goodbye"));
// "export const Foo =<template>Goodbye</template>>"
// ^ eaten space stray > ^So the constructor eats every leading U+FEFF, which means a file that legitimately starts with a ZWNBSP after its BOM loses it. No corruption, but it's a character someone typed. Options as I see them:
Went with 1 and documented it in the README. 2 is maybe fifteen lines plus tests if you'd rather keep the character. Genuinely don't mind either way. Separately, CI has never actually run on this PR. Both commits show |
| @@ -405,7 +409,8 @@ export class ParseResultStringUtils { | |||
| * @param {Buffer} buffer | |||
| */ | |||
| constructor(buffer) { | |||
There was a problem hiding this comment.
each parse result can have a BOM? how?
There was a problem hiding this comment.
They cannot, and my comment sent you down the wrong path. Sorry.
ParseResultStringUtils holds one buffer, the whole document. Parse results are just byte ranges pointing into it, they carry no text of their own. So the BOM is a property of that one buffer, once, not of anything per parse result.
Reworded:
// This holds the whole document, which parse results only point into, and
// it is public, so the buffer may be a caller's rather than a Transformer's.The reason it strips at all is the second half: the class is exported, so someone can do new ParseResultStringUtils(Buffer.from(src)) themselves without a Transformer involved, and then nothing upstream has stripped for them. Constructed by a Transformer the strip is a no-op, since the constructor already handled it.
526fdb9 to
1cc487d
Compare
f0df2bc to
95edb63
Compare
0.7.2 carries the BOM fix from ember-tooling/content-tag-utils#33, landed as stripBOM rather than the byte offset I proposed. The shipped contract is that coordinatesOf strips a leading BOM and returns indices into the stripped source, so its offsets now agree with content-tag's without either being handed a pre-stripped string. So the separate `body` copy goes. Both calls take `source` and the BOM's one code unit is added back once, because the block offsets and `codeBefore` index the original file and stylelint's --fix output has to keep the BOM where the author put it. The add-back was untested, which the earlier mutation pass missed. Losing it shifts the parse window one character left, but the text node offset inside that window shifts by the same amount, so the block's absolute start comes out identical and every existing fixture stayed green. The fixtures hid it a second way: their templates end in whitespace before </template>, so the character dropped off the end of the window did not matter either. The new test closes both. Its template ends without trailing space, making the window's last character the `>` of </style>, so a window one short fails to parse and the block is dropped with no warning and no error. Forcing bomLength to 0, or dropping the add-back from contentsEnd alone, now fails it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
content-tagstrips a leading byte order mark before parsing, so every byte offset it reports is relative to the stripped source.coordinatesOfandParseResultStringUtilsboth sliced the caller's original source by those offsets, and that source still had the BOM, so the whole window landed three bytes to the left.Found while adopting 0.7.0 in stylelint-ember-scoped-css, where
.gtsfiles are linted and a Windows-authored component has to keep working.coordinatesOfreturned indices into the wrong texttransformSyncsilently did nothingThis one is the reason I bothered, since nothing errors:
The callback got a shifted window (
"te>Hello th"rather than"Hello"), so the replacement found nothing to match and the file came back byte-identical.The change
Per @NullVoxPopuli's review, a BOM isn't part of the source anymore, rather than being something to offset around. Every entry point strips it on the way in, nothing hands one back, and nothing is logged or thrown.
Smaller change than what I had, too.
coordinates-of.jsandtransformer.jsare nowmainplus a line each instead of BOM arithmetic threaded through every slice. ThetrimStartthing sorted itself out as well:String#trimStartcounts U+FEFF as whitespace, so the old version had to explicitly exclude the BOM to stopcolumnOffsetclaiming a column of indent on a file with none. NowtrimStartnever sees one.One wrinkle
content-tagstrips exactly one BOM. It inherits that from swc, anifand a hardcoded three bytes, and there's no BOM handling in content-tag's own source at all. Sosrc/bom.jshas one helper per side of that parse, and they strip different amounts so that they compose:stripBomsBeforeParsetakes every leading BOM. It's for source on its way into a parser, so swc has nothing left to drop and its offsets land in exactly the string we kept. Used by theTransformerconstructor andunprocess.stripBomAfterParsetakes exactly one, matching swc, to move an already-parsed caller's source into the frame those offsets came from. Used bycoordinatesOfandParseResultStringUtils, both public and both able to receive a caller's buffer directly.Taking all of them up front is what leaves the single strip on the other side a no-op, and that's what lets
ParseResultStringUtilsstrip defensively for standalone callers without double-stripping when aTransformerbuilt it. Make both take one and they stack instead. Two BOMs through a strip-one-everywhere version mangles the document rather than failing quietly:Eaten space, stray
>. I didn't hit that on a real file, it fell out of reviewing this branch, but two BOMs reproduce it.Taking all of them has a cost: a second U+FEFF is really a ZWNBSP, so a document that legitimately starts with one loses it. I think that's the right trade at this size, but I'd rather say so than bury it.
What callers see
startandendindex the stripped source, so if you're holding a BOM'd string you can't slice it with those directly. New README section covers it, and theCoordinatestype says so too.line,columnandcolumnOffsetcome out identical for a file with a leading BOM and the same file without one.I left
stripBomsBeforeParseinternal instead of exporting it, since your whole point was that users shouldn't have to think about BOMs. Easy to export if you'd rather consumers normalize with the same helper.Tests
20 in
tests/bom.test.ts, across all six public entry points, 0 through 3 BOMs, class member and multi template documents, andBufferinput.Written test first, and I tried to be honest about which ones actually prove anything:
coordinatesOf's takes out the four coordinate tests, theTransformerconstructor's the five output tests,ParseResultStringUtils' just its own.unprocesstest and the class member / multi template ones passed on arrival, so they're coverage, not proof.unprocesspassed becauseember-estreealso drops a BOM on reprint. It strips explicitly now so the behavior is ours either way.pnpm testis 90 green.pnpm lintpasseslint:types,lint:packageandlint:published-types.Heads up that CI has never run here. The
CIworkflow sits ataction_requiredwith zero jobs on both commits, waiting on a maintainer to approve workflows for a fork PR, so an empty checks tab isn't telling you anything about the code.