@clossys/writer 0.3.6 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +89 -0
- package/dist/approved-copy-coverage-rate-cli.d.ts +10 -0
- package/dist/approved-copy-coverage-rate-cli.d.ts.map +1 -0
- package/dist/approved-copy-coverage-rate-cli.js +59 -0
- package/dist/approved-copy-coverage-rate-cli.js.map +1 -0
- package/dist/approved-copy-coverage-rate.d.ts +23 -0
- package/dist/approved-copy-coverage-rate.d.ts.map +1 -0
- package/dist/approved-copy-coverage-rate.js +153 -0
- package/dist/approved-copy-coverage-rate.js.map +1 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +105 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +30 -0
- package/dist/scan.js.map +1 -1
- package/package.json +10 -3
- package/src/approved-copy-coverage-rate-cli.ts +55 -0
- package/src/approved-copy-coverage-rate.ts +201 -0
- package/src/cli.ts +179 -26
- package/src/index.ts +6 -0
- package/src/scan.ts +31 -0
package/src/cli.ts
CHANGED
|
@@ -130,11 +130,11 @@ import {
|
|
|
130
130
|
type AddressabilityGateResult,
|
|
131
131
|
type AddressabilityScanResult,
|
|
132
132
|
} from "./addressability.js";
|
|
133
|
-
import { checkCopyTraceability, type CopyGateResult } from "./copy-gate.js";
|
|
133
|
+
import { checkCopyTraceability, type CopyGateFinding, type CopyGateIgnored, type CopyGateResult } from "./copy-gate.js";
|
|
134
134
|
import { checkLocaleCoverage, type LocaleCoverageReport } from "./locale-coverage.js";
|
|
135
135
|
import { checkPassageComposition, readPassageRecord, type PassageGateResult } from "./passage.js";
|
|
136
136
|
import { readCopyRecord } from "./registry.js";
|
|
137
|
-
import { scanCopySourceTree, type ScanResult } from "./scan.js";
|
|
137
|
+
import { scanCopySourceTree, type ScanResult, type UncheckedItem } from "./scan.js";
|
|
138
138
|
import { checkVoiceDerivationCoverage, type VoiceDerivationCoverageResult } from "./voice/index.js";
|
|
139
139
|
|
|
140
140
|
const USAGE = `Usage: writer-check <record-file> [scan-dir] [options]
|
|
@@ -154,8 +154,18 @@ are separate, stricter gates; neither runs as part of this default command.
|
|
|
154
154
|
|
|
155
155
|
Options:
|
|
156
156
|
--help Print this message and exit 0.
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
--format <text|json> Output format. Defaults to text. "json" prints exactly one
|
|
158
|
+
machine-readable object to stdout (see CopyTraceabilityReport in
|
|
159
|
+
cli.ts) instead of the human-readable report — "verdict" states the
|
|
160
|
+
same clean/findings/indeterminate outcome as the exit code, but
|
|
161
|
+
"findings" and "unchecked" are always both present, so a consumer
|
|
162
|
+
that only checks the exit code is never the only way to see what a
|
|
163
|
+
run DID measure: a scan that produced real findings AND one
|
|
164
|
+
unclassifiable construct in the same run (issue #753) still exits 2
|
|
165
|
+
(an indeterminate result must never read as clean), but its findings
|
|
166
|
+
remain reachable in this object rather than discarded with it.
|
|
167
|
+
|
|
168
|
+
Exit codes: 0 = clean, 1 = at least one finding, 2 = could not run (bad input, missing/invalid record, nothing matched to scan, every matched file failed to parse, or at least one JSX construct recognized but not reliably classified — see --format json above to still recover any findings a run like that DID produce).
|
|
159
169
|
|
|
160
170
|
Run "writer-check voice-derivation-coverage --help" for the second subcommand's own usage.
|
|
161
171
|
Run "writer-check locale-coverage --help" for the third subcommand's own usage.
|
|
@@ -196,19 +206,30 @@ export class CliInputError extends Error {}
|
|
|
196
206
|
interface ParsedArgs {
|
|
197
207
|
recordFile?: string;
|
|
198
208
|
scanDir?: string;
|
|
209
|
+
format: "text" | "json";
|
|
199
210
|
help: boolean;
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
203
214
|
let recordFile: string | undefined;
|
|
204
215
|
let scanDir: string | undefined;
|
|
216
|
+
let format: "text" | "json" = "text";
|
|
205
217
|
let help = false;
|
|
206
218
|
|
|
207
|
-
for (
|
|
219
|
+
for (let i = 0; i < argv.length; i++) {
|
|
220
|
+
const arg = argv[i] as string;
|
|
208
221
|
if (arg === "--help" || arg === "-h") {
|
|
209
222
|
help = true;
|
|
210
223
|
continue;
|
|
211
224
|
}
|
|
225
|
+
if (arg === "--format") {
|
|
226
|
+
const value = argv[++i];
|
|
227
|
+
if (value !== "text" && value !== "json") {
|
|
228
|
+
throw new CliInputError(`--format must be "text" or "json", got ${JSON.stringify(value)}`);
|
|
229
|
+
}
|
|
230
|
+
format = value;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
212
233
|
if (arg.startsWith("-")) {
|
|
213
234
|
throw new CliInputError(`unknown flag "${arg}"`);
|
|
214
235
|
}
|
|
@@ -221,7 +242,7 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
221
242
|
}
|
|
222
243
|
}
|
|
223
244
|
|
|
224
|
-
return { recordFile, scanDir, help };
|
|
245
|
+
return { recordFile, scanDir, format, help };
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
function requireDirectory(label: string, path: string): void {
|
|
@@ -706,6 +727,74 @@ function printAddressabilityReport(result: AddressabilityGateResult): void {
|
|
|
706
727
|
* that reason — `run()` below is the only caller that reads the real
|
|
707
728
|
* `process.argv`.
|
|
708
729
|
*/
|
|
730
|
+
/**
|
|
731
|
+
* The default command's own structured, machine-readable report — issue
|
|
732
|
+
* #753's second half. `printGateReport`/`printScanAccounting` already put
|
|
733
|
+
* every finding a run DID produce ahead of any exit-code decision in the
|
|
734
|
+
* text output; the gap #753 found is that a consumer keying on the exit
|
|
735
|
+
* code alone (2 = indeterminate) cannot reach those findings at all,
|
|
736
|
+
* because nothing besides that human-readable text carries them. This
|
|
737
|
+
* type is that carrier: `--format json` prints EXACTLY ONE of these,
|
|
738
|
+
* nothing else, to stdout, whatever `exitCode` turns out to be — the same
|
|
739
|
+
* "one parseable object regardless of verdict" contract `inspector`'s own
|
|
740
|
+
* `VerifyStandardsReport`/`--format json` already publishes (see
|
|
741
|
+
* `packages/inspector/src/cli.ts`), and the same shape `observer`'s
|
|
742
|
+
* fleet-coverage grader and `addressability.ts`'s own `AddressabilityGateResult`
|
|
743
|
+
* use: a per-item verdict (here, each `CopyGateFinding`/`UncheckedItem`) is
|
|
744
|
+
* a DIFFERENT question from the run's one overall verdict, and collapsing
|
|
745
|
+
* the two into a bare exit code is exactly what discarded 292 real
|
|
746
|
+
* findings alongside one unclassifiable construct in #753's own repro.
|
|
747
|
+
*
|
|
748
|
+
* `verdict` is the SAME three states this whole package uses everywhere
|
|
749
|
+
* else (`"clean" | "findings" | "indeterminate"`), computed with the
|
|
750
|
+
* identical precedence `main()`'s own exit-code decision already applies
|
|
751
|
+
* below — `unchecked.length > 0` wins over `findings.length > 0`, which
|
|
752
|
+
* wins over a clean pass. Never a fourth state, and never inferred
|
|
753
|
+
* separately from `exitCode` by a consumer — read `verdict` (or
|
|
754
|
+
* `exitCode`) and treat `findings`/`unchecked` as what was actually
|
|
755
|
+
* measured, present or empty independent of which verdict resulted.
|
|
756
|
+
*
|
|
757
|
+
* `reason` is populated ONLY for the "could not run at all" shape of
|
|
758
|
+
* indeterminate — a missing/invalid copy record, an untrustworthy
|
|
759
|
+
* exclusion list, or a scan that matched/parsed nothing — where `findings`
|
|
760
|
+
* and `unchecked` are necessarily both `[]` because `checkCopyTraceability`
|
|
761
|
+
* never ran. It is `undefined` whenever a real scan DID run, including the
|
|
762
|
+
* `"indeterminate"` case #753 is about (real `findings`, plus one or more
|
|
763
|
+
* `unchecked` entries) — that case needs no extra prose beyond the data
|
|
764
|
+
* itself, which is the entire point of shipping it as data.
|
|
765
|
+
*/
|
|
766
|
+
export interface CopyTraceabilityReport {
|
|
767
|
+
recordFile: string;
|
|
768
|
+
scanDir: string;
|
|
769
|
+
verdict: "clean" | "findings" | "indeterminate";
|
|
770
|
+
exitCode: 0 | 1 | 2;
|
|
771
|
+
reason?: string;
|
|
772
|
+
filesScanned: number;
|
|
773
|
+
candidatesScanned: number;
|
|
774
|
+
matched: number;
|
|
775
|
+
findings: CopyGateFinding[];
|
|
776
|
+
ignored: CopyGateIgnored[];
|
|
777
|
+
unchecked: UncheckedItem[];
|
|
778
|
+
parseFailures: { file: string; detail: string }[];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function emptyReport(recordFile: string, scanDir: string, exitCode: 0 | 1 | 2, reason: string): CopyTraceabilityReport {
|
|
782
|
+
return {
|
|
783
|
+
recordFile,
|
|
784
|
+
scanDir,
|
|
785
|
+
verdict: "indeterminate",
|
|
786
|
+
exitCode,
|
|
787
|
+
reason,
|
|
788
|
+
filesScanned: 0,
|
|
789
|
+
candidatesScanned: 0,
|
|
790
|
+
matched: 0,
|
|
791
|
+
findings: [],
|
|
792
|
+
ignored: [],
|
|
793
|
+
unchecked: [],
|
|
794
|
+
parseFailures: [],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
709
798
|
export function main(argv: string[]): number {
|
|
710
799
|
// Subcommand dispatch: only `argv[0] === "voice-derivation-coverage"` or
|
|
711
800
|
// `argv[0] === "locale-coverage"` exactly diverts to the second/third
|
|
@@ -729,13 +818,20 @@ export function main(argv: string[]): number {
|
|
|
729
818
|
throw new CliInputError("record-file is required");
|
|
730
819
|
}
|
|
731
820
|
|
|
821
|
+
const jsonMode = args.format === "json";
|
|
822
|
+
const emitJson = (report: CopyTraceabilityReport): void => {
|
|
823
|
+
if (jsonMode) console.log(JSON.stringify(report, null, 2));
|
|
824
|
+
};
|
|
825
|
+
|
|
732
826
|
const recordFile = resolve(args.recordFile);
|
|
733
827
|
const scanDir = resolve(args.scanDir ?? process.cwd());
|
|
734
828
|
requireFile("record-file", recordFile);
|
|
735
829
|
requireDirectory("scan-dir", scanDir);
|
|
736
830
|
|
|
737
|
-
|
|
738
|
-
|
|
831
|
+
if (!jsonMode) {
|
|
832
|
+
console.log(`Copy record: ${recordFile}`);
|
|
833
|
+
console.log(`Scan directory: ${scanDir}`);
|
|
834
|
+
}
|
|
739
835
|
|
|
740
836
|
// The copy record itself missing/unreadable/unparseable/invalid is
|
|
741
837
|
// fail-closed: there is no trustworthy set of registered entries to
|
|
@@ -744,9 +840,19 @@ export function main(argv: string[]): number {
|
|
|
744
840
|
// `strategy-facts-check` holds `facts.json` to.
|
|
745
841
|
const read = readCopyRecord(recordFile);
|
|
746
842
|
if (!read.complete || !read.record) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
843
|
+
if (!jsonMode) {
|
|
844
|
+
console.error(`\nCopy record could not be loaded:`);
|
|
845
|
+
for (const issue of read.issues) console.error(` [${issue.reason}] ${issue.detail}`);
|
|
846
|
+
console.error("Refusing to report a pass with no trustworthy copy record to check source against.");
|
|
847
|
+
}
|
|
848
|
+
emitJson(
|
|
849
|
+
emptyReport(
|
|
850
|
+
recordFile,
|
|
851
|
+
scanDir,
|
|
852
|
+
2,
|
|
853
|
+
`copy record could not be loaded: ${read.issues.map((i) => `[${i.reason}] ${i.detail}`).join("; ")}`,
|
|
854
|
+
),
|
|
855
|
+
);
|
|
750
856
|
return 2;
|
|
751
857
|
}
|
|
752
858
|
|
|
@@ -758,7 +864,7 @@ export function main(argv: string[]): number {
|
|
|
758
864
|
// own options) does not require touching this exit-code logic again —
|
|
759
865
|
// see this file's top doc comment.
|
|
760
866
|
const scan = scanCopySourceTree(scanDir); // throws (fail-closed) on an unreadable directory — caught by run()
|
|
761
|
-
printScanAccounting(scan);
|
|
867
|
+
if (!jsonMode) printScanAccounting(scan);
|
|
762
868
|
|
|
763
869
|
// A malformed pathExclusions entry means this run cannot trust which
|
|
764
870
|
// files were correctly excluded — "could not run", not "ran and found
|
|
@@ -767,8 +873,13 @@ export function main(argv: string[]): number {
|
|
|
767
873
|
// walk itself found anything.
|
|
768
874
|
const invalidPathExclusions = scan.pathExclusionFindings.filter((f) => f.severity === "error");
|
|
769
875
|
if (invalidPathExclusions.length > 0) {
|
|
770
|
-
|
|
771
|
-
|
|
876
|
+
if (!jsonMode) {
|
|
877
|
+
console.error(
|
|
878
|
+
`\n${invalidPathExclusions.length} pathExclusions entr${invalidPathExclusions.length === 1 ? "y is" : "ies are"} invalid — refusing to report a pass built on an exclusion list that cannot be trusted.`,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
emitJson(
|
|
882
|
+
emptyReport(recordFile, scanDir, 2, `${invalidPathExclusions.length} invalid pathExclusions entr(y/ies)`),
|
|
772
883
|
);
|
|
773
884
|
return 2;
|
|
774
885
|
}
|
|
@@ -779,12 +890,19 @@ export function main(argv: string[]): number {
|
|
|
779
890
|
// scan.parseFailures) are both, deliberately, the same outcome here —
|
|
780
891
|
// neither one is "scanned everything, found nothing wrong".
|
|
781
892
|
if (scan.filesScanned === 0) {
|
|
782
|
-
|
|
893
|
+
const reason =
|
|
783
894
|
scan.parseFailures.length > 0
|
|
784
|
-
?
|
|
785
|
-
:
|
|
786
|
-
)
|
|
787
|
-
|
|
895
|
+
? `every file matched under "${scanDir}" failed to parse — nothing was actually scanned`
|
|
896
|
+
: `no files matched under "${scanDir}" — nothing was scanned`;
|
|
897
|
+
if (!jsonMode) {
|
|
898
|
+
console.error(
|
|
899
|
+
scan.parseFailures.length > 0
|
|
900
|
+
? `\nEvery file matched under "${scanDir}" failed to parse — nothing was actually scanned.`
|
|
901
|
+
: `\nNo files matched under "${scanDir}" — nothing was scanned.`,
|
|
902
|
+
);
|
|
903
|
+
console.error("Refusing to report a pass for a scan that checked nothing.");
|
|
904
|
+
}
|
|
905
|
+
emitJson(emptyReport(recordFile, scanDir, 2, reason));
|
|
788
906
|
return 2;
|
|
789
907
|
}
|
|
790
908
|
|
|
@@ -807,15 +925,25 @@ export function main(argv: string[]): number {
|
|
|
807
925
|
// read as fully accounted-for, it does not hide anything that was
|
|
808
926
|
// learned.
|
|
809
927
|
if (scan.parseFailures.length > 0) {
|
|
810
|
-
|
|
811
|
-
|
|
928
|
+
if (!jsonMode) {
|
|
929
|
+
console.error(
|
|
930
|
+
`\n${scan.parseFailures.length} of ${scan.parseFailures.length + scan.filesScanned} matched file(s) under "${scanDir}" could not be parsed and were never examined for copy.`,
|
|
931
|
+
);
|
|
932
|
+
console.error("Refusing to report a pass for a scan whose coverage is incomplete.");
|
|
933
|
+
}
|
|
934
|
+
emitJson(
|
|
935
|
+
emptyReport(
|
|
936
|
+
recordFile,
|
|
937
|
+
scanDir,
|
|
938
|
+
2,
|
|
939
|
+
`${scan.parseFailures.length} of ${scan.parseFailures.length + scan.filesScanned} matched file(s) could not be parsed and were never examined for copy`,
|
|
940
|
+
),
|
|
812
941
|
);
|
|
813
|
-
console.error("Refusing to report a pass for a scan whose coverage is incomplete.");
|
|
814
942
|
return 2;
|
|
815
943
|
}
|
|
816
944
|
|
|
817
945
|
const result = checkCopyTraceability(scan.candidates, scan.citations, read.record, scan.filesScanned, scan.unchecked);
|
|
818
|
-
printGateReport(result);
|
|
946
|
+
if (!jsonMode) printGateReport(result);
|
|
819
947
|
|
|
820
948
|
// `unchecked` wins over everything else in the exit-code decision — see
|
|
821
949
|
// this file's own top doc comment for why it is a `2`, not a `1`: it
|
|
@@ -824,9 +952,34 @@ export function main(argv: string[]): number {
|
|
|
824
952
|
// finding was still printed above, so nothing real is hidden — this
|
|
825
953
|
// only refuses to let the run as a whole read as clean or as
|
|
826
954
|
// fully-accounted-for.
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
955
|
+
//
|
|
956
|
+
// THIS is #753's own partial-evaluation case: `result.findings` can be
|
|
957
|
+
// (and in #753's own repro, was) non-empty at the exact same time as
|
|
958
|
+
// `result.unchecked` — a real 292-finding scan sitting right next to one
|
|
959
|
+
// unclassifiable construct. `verdict` is still, correctly,
|
|
960
|
+
// `"indeterminate"` (never `"clean"`, never silently `"findings"`) —
|
|
961
|
+
// but unlike a bare exit code, `--format json`'s report carries
|
|
962
|
+
// `findings` (all 292 of them) AND `unchecked` (the one construct) in
|
|
963
|
+
// the SAME object, so a consumer keying on `exitCode`/`verdict` alone
|
|
964
|
+
// still fails closed exactly as this package intends, while a consumer
|
|
965
|
+
// that reads further is never forced to throw away what was measured.
|
|
966
|
+
const exitCode: 0 | 1 | 2 = result.unchecked.length > 0 ? 2 : result.findings.length > 0 ? 1 : 0;
|
|
967
|
+
const verdict: CopyTraceabilityReport["verdict"] =
|
|
968
|
+
result.unchecked.length > 0 ? "indeterminate" : result.findings.length > 0 ? "findings" : "clean";
|
|
969
|
+
emitJson({
|
|
970
|
+
recordFile,
|
|
971
|
+
scanDir,
|
|
972
|
+
verdict,
|
|
973
|
+
exitCode,
|
|
974
|
+
filesScanned: scan.filesScanned,
|
|
975
|
+
candidatesScanned: result.candidatesScanned,
|
|
976
|
+
matched: result.matched,
|
|
977
|
+
findings: result.findings,
|
|
978
|
+
ignored: result.ignored,
|
|
979
|
+
unchecked: result.unchecked,
|
|
980
|
+
parseFailures: scan.parseFailures.map((p) => ({ file: p.file, detail: p.detail })),
|
|
981
|
+
});
|
|
982
|
+
return exitCode;
|
|
830
983
|
}
|
|
831
984
|
|
|
832
985
|
function run(): void {
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,12 @@ export { readCopyRecord } from "./registry.js";
|
|
|
54
54
|
export type { CopyRegistryReadIssue, CopyRegistryReadIssueReason, CopyRegistryReadResult } from "./registry.js";
|
|
55
55
|
|
|
56
56
|
export { checkCopyRecord } from "./checker.js";
|
|
57
|
+
export { assessApprovedCopyCoverageRate } from "./approved-copy-coverage-rate.js";
|
|
58
|
+
export type {
|
|
59
|
+
ApprovedCopyCoverageRateAssessment,
|
|
60
|
+
ApprovedCopyCoverageRateFinding,
|
|
61
|
+
ApprovedCopyCoverageRateState,
|
|
62
|
+
} from "./approved-copy-coverage-rate.js";
|
|
57
63
|
export type {
|
|
58
64
|
CopyEntryCheckResult,
|
|
59
65
|
CopyEntrySkip,
|
package/src/scan.ts
CHANGED
|
@@ -1319,6 +1319,37 @@ function tokenize(content: string, isJsxFile: boolean): TokenizeResult {
|
|
|
1319
1319
|
if (j >= n) return backtrack(); // ran off the end before the head ever closed — not committed, silently not-JSX
|
|
1320
1320
|
|
|
1321
1321
|
const cj = content[j] as string;
|
|
1322
|
+
|
|
1323
|
+
// Comment trivia BETWEEN attributes (`//` line comment or `/* */`
|
|
1324
|
+
// block comment) — legal TSX a formatter routinely produces
|
|
1325
|
+
// (issue #753). Checked before the self-closing `/>` test below,
|
|
1326
|
+
// since both start with `/`: `content[j + 1] === ">"` is the
|
|
1327
|
+
// self-close, anything else after a `/` here is a comment, never
|
|
1328
|
+
// a division operator — there is no expression context inside an
|
|
1329
|
+
// opening tag's attribute list for `/` to divide. Treated exactly
|
|
1330
|
+
// like the whitespace just skipped above: this function has not
|
|
1331
|
+
// committed to "this is real JSX" yet (no `>`/`/>` found), so
|
|
1332
|
+
// consuming a comment here is silent trivia-skipping, not a
|
|
1333
|
+
// reportable event — matching every other pre-commit case in
|
|
1334
|
+
// this loop (e.g. the TSX generic-arrow-function `,` below,
|
|
1335
|
+
// which also backtracks with no message). An unterminated `/*`
|
|
1336
|
+
// is the one exception: it means this position can never
|
|
1337
|
+
// resolve to a valid tag head, so this reports the SAME "not
|
|
1338
|
+
// JSX after all" backtrack as running off the end of the file,
|
|
1339
|
+
// above.
|
|
1340
|
+
if (cj === "/" && content[j + 1] === "/") {
|
|
1341
|
+
const nl = content.indexOf("\n", j);
|
|
1342
|
+
j = nl === -1 ? n : nl;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (cj === "/" && content[j + 1] === "*") {
|
|
1346
|
+
const close = content.indexOf("*/", j + 2);
|
|
1347
|
+
if (close === -1) return backtrack(); // unterminated block comment — not committed, silently not-JSX
|
|
1348
|
+
advanceLine(j, close + 2);
|
|
1349
|
+
j = close + 2;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1322
1353
|
if (cj === "/" && content[j + 1] === ">") {
|
|
1323
1354
|
selfClosing = true;
|
|
1324
1355
|
j += 2;
|