jscpd-rs 0.1.0

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/Cargo.lock +1323 -0
  3. package/Cargo.toml +54 -0
  4. package/LICENSE +21 -0
  5. package/README.md +372 -0
  6. package/docs/api-parity.md +49 -0
  7. package/docs/cloning-plan.md +281 -0
  8. package/docs/compat-baseline.md +535 -0
  9. package/docs/format-porting.md +86 -0
  10. package/docs/junior-task-template.md +62 -0
  11. package/docs/junior-workflow.md +87 -0
  12. package/docs/migrating-from-jscpd.md +193 -0
  13. package/docs/npm-release.md +116 -0
  14. package/docs/public-benchmark-suite.md +81 -0
  15. package/docs/release-checklist.md +200 -0
  16. package/docs/release-decisions.md +103 -0
  17. package/docs/release-readiness.md +51 -0
  18. package/docs/upstream-bugs.md +501 -0
  19. package/docs/upstream-issue-drafts.md +393 -0
  20. package/docs/user-guide.md +309 -0
  21. package/examples/dump_oxc_tokens.rs +112 -0
  22. package/examples/library_api.rs +42 -0
  23. package/npm/bin/jscpd-rs.js +6 -0
  24. package/npm/bin/jscpd-server.js +6 -0
  25. package/npm/lib/run-binary.js +68 -0
  26. package/npm/scripts/postinstall.js +50 -0
  27. package/package.json +53 -0
  28. package/skills/dry-refactoring/SKILL.md +63 -0
  29. package/skills/jscpd/SKILL.md +85 -0
  30. package/src/app.rs +512 -0
  31. package/src/bin/jscpd-server.rs +429 -0
  32. package/src/blame.rs +130 -0
  33. package/src/cli/config.rs +543 -0
  34. package/src/cli/parsing.rs +301 -0
  35. package/src/cli/tests.rs +543 -0
  36. package/src/cli.rs +671 -0
  37. package/src/detector/matching/secondary.rs +387 -0
  38. package/src/detector/matching.rs +274 -0
  39. package/src/detector/model.rs +190 -0
  40. package/src/detector/prepare.rs +71 -0
  41. package/src/detector/skip_local.rs +40 -0
  42. package/src/detector/statistics.rs +138 -0
  43. package/src/detector/store.rs +96 -0
  44. package/src/detector/tests.rs +238 -0
  45. package/src/detector.rs +265 -0
  46. package/src/files/discovery.rs +508 -0
  47. package/src/files/gitignore.rs +203 -0
  48. package/src/files/paths.rs +68 -0
  49. package/src/files/shebang.rs +106 -0
  50. package/src/files/tests.rs +523 -0
  51. package/src/files.rs +25 -0
  52. package/src/formats.rs +570 -0
  53. package/src/lib.rs +433 -0
  54. package/src/main.rs +26 -0
  55. package/src/report/ai.rs +125 -0
  56. package/src/report/badge.rs +238 -0
  57. package/src/report/console.rs +180 -0
  58. package/src/report/console_common.rs +37 -0
  59. package/src/report/console_full.rs +139 -0
  60. package/src/report/csv.rs +65 -0
  61. package/src/report/escape.rs +8 -0
  62. package/src/report/file_output.rs +28 -0
  63. package/src/report/html/assets.rs +47 -0
  64. package/src/report/html.rs +336 -0
  65. package/src/report/json.rs +119 -0
  66. package/src/report/markdown.rs +125 -0
  67. package/src/report/sarif.rs +302 -0
  68. package/src/report/silent.rs +22 -0
  69. package/src/report/source.rs +38 -0
  70. package/src/report/summary.rs +50 -0
  71. package/src/report/test_support.rs +133 -0
  72. package/src/report/threshold.rs +76 -0
  73. package/src/report/xcode.rs +90 -0
  74. package/src/report/xml.rs +119 -0
  75. package/src/report.rs +250 -0
  76. package/src/server/mcp.rs +942 -0
  77. package/src/server.rs +1081 -0
  78. package/src/tokenizer/apex.rs +97 -0
  79. package/src/tokenizer/blocks.rs +532 -0
  80. package/src/tokenizer/embedded.rs +106 -0
  81. package/src/tokenizer/generic.rs +511 -0
  82. package/src/tokenizer/hash.rs +27 -0
  83. package/src/tokenizer/ignore.rs +33 -0
  84. package/src/tokenizer/line_index.rs +33 -0
  85. package/src/tokenizer/markdown.rs +289 -0
  86. package/src/tokenizer/markup_attrs.rs +289 -0
  87. package/src/tokenizer/oxc/fallback.rs +275 -0
  88. package/src/tokenizer/oxc/jsx.rs +168 -0
  89. package/src/tokenizer/oxc/kind.rs +177 -0
  90. package/src/tokenizer/oxc/lexical.rs +67 -0
  91. package/src/tokenizer/oxc.rs +659 -0
  92. package/src/tokenizer/scan.rs +88 -0
  93. package/src/tokenizer/tap.rs +150 -0
  94. package/src/tokenizer/tests.rs +915 -0
  95. package/src/tokenizer.rs +328 -0
  96. package/src/verbose.rs +195 -0
@@ -0,0 +1,289 @@
1
+ use std::path::Path;
2
+
3
+ use crate::cli::Options;
4
+ use crate::formats;
5
+
6
+ use super::embedded::{
7
+ assign_sequential_positions, blank_ranges_preserve_newlines, offset_tokens,
8
+ tokenize_generic_with_whitespace,
9
+ };
10
+ use super::scan::line_spans;
11
+ use super::{
12
+ ByteSpan, DetectionToken, LineIndex, TokenContext, TokenKind, TokenMap, find_ignore_regions,
13
+ is_oxc_format, push_token, tokenize_generic, tokenize_oxc_maps,
14
+ };
15
+
16
+ pub(super) fn tokenize_maps(
17
+ content: &str,
18
+ options: &Options,
19
+ ignore_regions: &[[usize; 2]],
20
+ ) -> Vec<TokenMap> {
21
+ let mut fences = markdown_fenced_code_blocks(content, options);
22
+ if let Some(front_matter) = markdown_front_matter_block(content) {
23
+ fences.push(front_matter);
24
+ fences.sort_by_key(|fence| fence.block_start);
25
+ }
26
+ let sanitized = blank_ranges_preserve_newlines(
27
+ content,
28
+ fences
29
+ .iter()
30
+ .map(|fence| [fence.block_start, fence.block_end])
31
+ .collect::<Vec<_>>()
32
+ .as_slice(),
33
+ );
34
+ let mut maps = Vec::new();
35
+ let line_index = LineIndex::new(content);
36
+ let mut markdown_tokens = tokenize_generic(&sanitized, "markdown", options, ignore_regions);
37
+ push_markdown_fence_gap_tokens(
38
+ &mut markdown_tokens,
39
+ &sanitized,
40
+ &fences,
41
+ options,
42
+ ignore_regions,
43
+ &line_index,
44
+ );
45
+ if !markdown_tokens.is_empty() {
46
+ markdown_tokens.sort_by_key(|token| (token.range[0], token.range[1]));
47
+ maps.push(TokenMap {
48
+ format: "markdown".to_string(),
49
+ tokens: markdown_tokens,
50
+ positions_assigned: false,
51
+ });
52
+ }
53
+
54
+ let mut embedded_maps = std::collections::BTreeMap::<String, Vec<DetectionToken>>::new();
55
+ for fence in fences {
56
+ let inner = &content[fence.inner_start..fence.inner_end];
57
+ let inner_ignore_regions = find_ignore_regions(inner, options);
58
+ let inner_maps = if is_oxc_format(&fence.format) {
59
+ tokenize_oxc_maps(inner, &fence.format, options, &inner_ignore_regions)
60
+ } else if fence.format == "yaml" {
61
+ vec![TokenMap {
62
+ format: fence.format.clone(),
63
+ tokens: tokenize_generic(inner, &fence.format, options, &inner_ignore_regions),
64
+ positions_assigned: false,
65
+ }]
66
+ } else {
67
+ vec![TokenMap {
68
+ format: fence.format.clone(),
69
+ tokens: tokenize_generic_with_whitespace(
70
+ inner,
71
+ &fence.format,
72
+ options,
73
+ &inner_ignore_regions,
74
+ ),
75
+ positions_assigned: false,
76
+ }]
77
+ };
78
+ let inner_start = line_index.location(fence.inner_start);
79
+ for mut map in inner_maps {
80
+ offset_tokens(&mut map.tokens, fence.inner_start, &inner_start);
81
+ embedded_maps
82
+ .entry(map.format)
83
+ .or_default()
84
+ .extend(map.tokens);
85
+ }
86
+ }
87
+
88
+ for (format, mut tokens) in embedded_maps {
89
+ assign_sequential_positions(&mut tokens);
90
+ maps.push(TokenMap {
91
+ format,
92
+ tokens,
93
+ positions_assigned: true,
94
+ });
95
+ }
96
+
97
+ maps
98
+ }
99
+
100
+ #[derive(Debug)]
101
+ struct MarkdownFence {
102
+ format: String,
103
+ front_matter: bool,
104
+ block_start: usize,
105
+ inner_start: usize,
106
+ inner_end: usize,
107
+ block_end: usize,
108
+ }
109
+
110
+ fn markdown_fenced_code_blocks(content: &str, options: &Options) -> Vec<MarkdownFence> {
111
+ let lines = line_spans(content);
112
+ let mut fences = Vec::new();
113
+ let mut idx = 0usize;
114
+ while idx < lines.len() {
115
+ let line = &content[lines[idx].start..lines[idx].end];
116
+ let Some(open) = markdown_opening_fence(line) else {
117
+ idx += 1;
118
+ continue;
119
+ };
120
+ let Some(format) = resolve_markdown_fence_format(open.info, options) else {
121
+ idx += 1;
122
+ continue;
123
+ };
124
+ let Some(close_idx) = lines[idx + 1..]
125
+ .iter()
126
+ .position(|span| markdown_closing_fence(&content[span.start..span.end], &open))
127
+ .map(|position| idx + 1 + position)
128
+ else {
129
+ idx += 1;
130
+ continue;
131
+ };
132
+ let inner_start = lines
133
+ .get(idx + 1)
134
+ .map(|span| span.start)
135
+ .unwrap_or(lines[idx].next_start);
136
+ let inner_end = content[..lines[close_idx].start]
137
+ .strip_suffix('\n')
138
+ .map(|prefix| prefix.len())
139
+ .unwrap_or(lines[close_idx].start);
140
+ fences.push(MarkdownFence {
141
+ format,
142
+ front_matter: false,
143
+ block_start: lines[idx].start,
144
+ inner_start,
145
+ inner_end: inner_end.max(inner_start),
146
+ block_end: lines[close_idx].next_start.min(content.len()),
147
+ });
148
+ idx = close_idx + 1;
149
+ }
150
+ fences
151
+ }
152
+
153
+ fn markdown_front_matter_block(content: &str) -> Option<MarkdownFence> {
154
+ if !(content.starts_with("---\n") || content.starts_with("---\r\n")) {
155
+ return None;
156
+ }
157
+ let lines = line_spans(content);
158
+ let close_idx = lines
159
+ .iter()
160
+ .enumerate()
161
+ .skip(1)
162
+ .find(|(_, span)| {
163
+ let line = content[span.start..span.end].trim();
164
+ line == "---" || line == "..."
165
+ })
166
+ .map(|(idx, _)| idx)?;
167
+ let inner_start = lines.get(1)?.start;
168
+ let inner_end = content[..lines[close_idx].start]
169
+ .strip_suffix('\n')
170
+ .map(|prefix| prefix.len())
171
+ .unwrap_or(lines[close_idx].start);
172
+ Some(MarkdownFence {
173
+ format: "yaml".to_string(),
174
+ front_matter: true,
175
+ block_start: 0,
176
+ inner_start,
177
+ inner_end: inner_end.max(inner_start),
178
+ block_end: lines[close_idx].next_start.min(content.len()),
179
+ })
180
+ }
181
+
182
+ fn push_markdown_fence_gap_tokens(
183
+ tokens: &mut Vec<DetectionToken>,
184
+ sanitized: &str,
185
+ fences: &[MarkdownFence],
186
+ options: &Options,
187
+ ignore_regions: &[[usize; 2]],
188
+ line_index: &LineIndex,
189
+ ) {
190
+ let context = TokenContext {
191
+ content: sanitized,
192
+ options,
193
+ ignore_regions,
194
+ };
195
+ for fence in fences.iter().filter(|fence| !fence.front_matter) {
196
+ let mut start = fence.block_start;
197
+ while start < fence.block_end {
198
+ let ch = sanitized[start..].chars().next().unwrap_or('\0');
199
+ if !ch.is_whitespace() {
200
+ start += ch.len_utf8();
201
+ continue;
202
+ }
203
+ let (end, kind) = scan_markdown_gap_whitespace(sanitized, start, fence.block_end);
204
+ // Upstream Prism keeps same-line whitespace spans inside blanked
205
+ // fences, but starting clones on newline tokens shifts report
206
+ // starts to the previous line.
207
+ if kind == TokenKind::NewLine {
208
+ start = end.max(start + ch.len_utf8());
209
+ continue;
210
+ }
211
+ push_token(
212
+ tokens,
213
+ &context,
214
+ kind,
215
+ ByteSpan { start, end },
216
+ line_index.location(start),
217
+ line_index.location(end),
218
+ );
219
+ start = end.max(start + ch.len_utf8());
220
+ }
221
+ }
222
+ }
223
+
224
+ fn scan_markdown_gap_whitespace(content: &str, start: usize, limit: usize) -> (usize, TokenKind) {
225
+ if content.as_bytes()[start] == b'\n' {
226
+ return (start + 1, TokenKind::NewLine);
227
+ }
228
+ let mut end = start;
229
+ while end < limit {
230
+ let ch = content[end..].chars().next().unwrap_or('\0');
231
+ if ch == '\n' || !ch.is_whitespace() {
232
+ break;
233
+ }
234
+ end += ch.len_utf8();
235
+ }
236
+ (end, TokenKind::Empty)
237
+ }
238
+
239
+ struct MarkdownFenceOpen<'a> {
240
+ marker: u8,
241
+ len: usize,
242
+ info: &'a str,
243
+ }
244
+
245
+ fn markdown_opening_fence(line: &str) -> Option<MarkdownFenceOpen<'_>> {
246
+ let bytes = line.as_bytes();
247
+ let marker = *bytes.first()?;
248
+ if !matches!(marker, b'`' | b'~') {
249
+ return None;
250
+ }
251
+ let len = bytes.iter().take_while(|byte| **byte == marker).count();
252
+ if len < 3 {
253
+ return None;
254
+ }
255
+ Some(MarkdownFenceOpen {
256
+ marker,
257
+ len,
258
+ info: line[len..].trim(),
259
+ })
260
+ }
261
+
262
+ fn markdown_closing_fence(line: &str, open: &MarkdownFenceOpen<'_>) -> bool {
263
+ let bytes = line.as_bytes();
264
+ let len = bytes
265
+ .iter()
266
+ .take_while(|byte| **byte == open.marker)
267
+ .count();
268
+ len >= open.len && bytes[len..].iter().all(|byte| matches!(byte, b' ' | b'\t'))
269
+ }
270
+
271
+ fn resolve_markdown_fence_format(info: &str, options: &Options) -> Option<String> {
272
+ let tag = info.split_whitespace().next()?.to_ascii_lowercase();
273
+ let mapped = match tag.as_str() {
274
+ "node" => Some("javascript"),
275
+ "shell" | "zsh" => Some("bash"),
276
+ "golang" => Some("go"),
277
+ _ => formats::format_for_path(
278
+ Path::new(&format!("code.{tag}")),
279
+ &options.formats_exts,
280
+ &options.formats_names,
281
+ )
282
+ .or_else(|| {
283
+ formats::supported_formats()
284
+ .contains(&tag.as_str())
285
+ .then_some(tag.as_str())
286
+ }),
287
+ }?;
288
+ Some(mapped.to_string())
289
+ }
@@ -0,0 +1,289 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use crate::cli::Options;
4
+
5
+ use super::{ByteSpan, DetectionToken, LineIndex, TokenContext, TokenKind, push_token};
6
+
7
+ #[derive(Clone, Copy, Debug)]
8
+ pub(super) struct InlineStyleAttr {
9
+ pub(super) attr_start: usize,
10
+ name_start: usize,
11
+ name_end: usize,
12
+ value_start: usize,
13
+ value_end: usize,
14
+ attr_end: usize,
15
+ }
16
+
17
+ pub(super) fn inline_style_attr_ranges(attrs: &[InlineStyleAttr]) -> Vec<[usize; 2]> {
18
+ attrs
19
+ .iter()
20
+ .map(|attr| [attr.attr_start, attr.attr_end])
21
+ .collect()
22
+ }
23
+
24
+ pub(super) fn append_inline_style_attr_tokens(
25
+ grouped: &mut BTreeMap<String, Vec<DetectionToken>>,
26
+ content: &str,
27
+ attrs: &[InlineStyleAttr],
28
+ options: &Options,
29
+ ignore_regions: &[[usize; 2]],
30
+ line_index: &LineIndex,
31
+ ) {
32
+ if attrs.is_empty() {
33
+ return;
34
+ }
35
+
36
+ let context = TokenContext {
37
+ content,
38
+ options,
39
+ ignore_regions,
40
+ };
41
+ let css_tokens = grouped.entry("css".to_string()).or_default();
42
+ for attr in attrs {
43
+ push_inline_style_token(
44
+ css_tokens,
45
+ &context,
46
+ line_index,
47
+ TokenKind::Default,
48
+ attr.attr_start,
49
+ attr.name_start,
50
+ );
51
+ push_inline_style_token(
52
+ css_tokens,
53
+ &context,
54
+ line_index,
55
+ TokenKind::Default,
56
+ attr.name_start,
57
+ attr.name_end,
58
+ );
59
+ push_inline_style_token(
60
+ css_tokens,
61
+ &context,
62
+ line_index,
63
+ TokenKind::Punctuation,
64
+ attr.name_end,
65
+ attr.value_start,
66
+ );
67
+ append_inline_css_value_tokens(css_tokens, &context, line_index, attr);
68
+ push_inline_style_token(
69
+ css_tokens,
70
+ &context,
71
+ line_index,
72
+ TokenKind::Punctuation,
73
+ attr.value_end,
74
+ attr.attr_end,
75
+ );
76
+ }
77
+ }
78
+
79
+ fn append_inline_css_value_tokens(
80
+ tokens: &mut Vec<DetectionToken>,
81
+ context: &TokenContext<'_>,
82
+ line_index: &LineIndex,
83
+ attr: &InlineStyleAttr,
84
+ ) {
85
+ let mut cursor = attr.value_start;
86
+ while cursor < attr.value_end {
87
+ let ch = context.content[cursor..].chars().next().unwrap_or('\0');
88
+ if inline_css_punctuation(ch) {
89
+ let end = cursor + ch.len_utf8();
90
+ push_inline_style_token(
91
+ tokens,
92
+ context,
93
+ line_index,
94
+ TokenKind::Punctuation,
95
+ cursor,
96
+ end,
97
+ );
98
+ cursor = end;
99
+ continue;
100
+ }
101
+
102
+ let start = cursor;
103
+ cursor += ch.len_utf8();
104
+ while cursor < attr.value_end {
105
+ let ch = context.content[cursor..].chars().next().unwrap_or('\0');
106
+ if inline_css_punctuation(ch) {
107
+ break;
108
+ }
109
+ cursor += ch.len_utf8();
110
+ }
111
+ push_inline_style_token(
112
+ tokens,
113
+ context,
114
+ line_index,
115
+ TokenKind::Default,
116
+ start,
117
+ cursor,
118
+ );
119
+ }
120
+ }
121
+
122
+ fn push_inline_style_token(
123
+ tokens: &mut Vec<DetectionToken>,
124
+ context: &TokenContext<'_>,
125
+ line_index: &LineIndex,
126
+ kind: TokenKind,
127
+ start: usize,
128
+ end: usize,
129
+ ) {
130
+ if start >= end {
131
+ return;
132
+ }
133
+ push_token(
134
+ tokens,
135
+ context,
136
+ kind,
137
+ ByteSpan { start, end },
138
+ line_index.location(start),
139
+ line_index.location(end),
140
+ );
141
+ }
142
+
143
+ fn inline_css_punctuation(ch: char) -> bool {
144
+ matches!(ch, ':' | ';' | '{' | '}' | '(' | ')')
145
+ }
146
+
147
+ pub(super) fn find_inline_style_attrs(content: &str) -> Vec<InlineStyleAttr> {
148
+ let bytes = content.as_bytes();
149
+ let mut attrs = Vec::new();
150
+ let mut cursor = 0usize;
151
+
152
+ while let Some(open_offset) = content[cursor..].find('<') {
153
+ let tag_start = cursor + open_offset;
154
+ let tag_kind = bytes.get(tag_start + 1).copied();
155
+ if tag_kind.is_some_and(|byte| matches!(byte, b'/' | b'!' | b'?')) {
156
+ cursor = tag_start + 1;
157
+ continue;
158
+ }
159
+
160
+ let Some(tag_end) = find_opening_tag_end(bytes, tag_start + 1) else {
161
+ break;
162
+ };
163
+ collect_style_attrs_in_tag(content, tag_start + 1, tag_end, &mut attrs);
164
+ cursor = tag_end + 1;
165
+ }
166
+
167
+ attrs
168
+ }
169
+
170
+ fn collect_style_attrs_in_tag(
171
+ content: &str,
172
+ tag_content_start: usize,
173
+ tag_end: usize,
174
+ attrs: &mut Vec<InlineStyleAttr>,
175
+ ) {
176
+ let bytes = content.as_bytes();
177
+ let mut cursor = tag_content_start;
178
+
179
+ while cursor < tag_end && is_html_name_byte(bytes[cursor]) {
180
+ cursor += 1;
181
+ }
182
+
183
+ while cursor < tag_end {
184
+ let attr_start = cursor;
185
+ cursor = skip_ascii_whitespace_until(bytes, cursor, tag_end);
186
+ if cursor >= tag_end || bytes[cursor] == b'/' {
187
+ break;
188
+ }
189
+
190
+ let name_start = cursor;
191
+ while cursor < tag_end && is_html_attr_name_byte(bytes[cursor]) {
192
+ cursor += 1;
193
+ }
194
+ let name_end = cursor;
195
+ if name_start == name_end {
196
+ cursor += 1;
197
+ continue;
198
+ }
199
+
200
+ cursor = skip_ascii_whitespace_until(bytes, cursor, tag_end);
201
+ if bytes.get(cursor) != Some(&b'=') {
202
+ continue;
203
+ }
204
+ cursor = skip_ascii_whitespace_until(bytes, cursor + 1, tag_end);
205
+ let Some(quote) = bytes.get(cursor).copied() else {
206
+ break;
207
+ };
208
+ if !matches!(quote, b'\'' | b'"') {
209
+ cursor = scan_unquoted_attr_value(bytes, cursor, tag_end);
210
+ continue;
211
+ }
212
+
213
+ let quote_start = cursor;
214
+ let value_start = quote_start + 1;
215
+ let Some(value_end) = find_quoted_attr_end(bytes, value_start, tag_end, quote) else {
216
+ break;
217
+ };
218
+ let attr_end = value_end + 1;
219
+ if bytes[name_start..name_end].eq_ignore_ascii_case(b"style") {
220
+ attrs.push(InlineStyleAttr {
221
+ attr_start,
222
+ name_start,
223
+ name_end,
224
+ value_start,
225
+ value_end,
226
+ attr_end,
227
+ });
228
+ }
229
+ cursor = attr_end;
230
+ }
231
+ }
232
+
233
+ fn find_opening_tag_end(bytes: &[u8], mut cursor: usize) -> Option<usize> {
234
+ let mut quote = None;
235
+ while cursor < bytes.len() {
236
+ let byte = bytes[cursor];
237
+ if let Some(active_quote) = quote {
238
+ if byte == b'\\' {
239
+ cursor = (cursor + 2).min(bytes.len());
240
+ continue;
241
+ }
242
+ if byte == active_quote {
243
+ quote = None;
244
+ }
245
+ } else if matches!(byte, b'\'' | b'"') {
246
+ quote = Some(byte);
247
+ } else if byte == b'>' {
248
+ return Some(cursor);
249
+ }
250
+ cursor += 1;
251
+ }
252
+ None
253
+ }
254
+
255
+ fn find_quoted_attr_end(bytes: &[u8], mut cursor: usize, limit: usize, quote: u8) -> Option<usize> {
256
+ while cursor < limit {
257
+ if bytes[cursor] == b'\\' {
258
+ cursor = (cursor + 2).min(limit);
259
+ continue;
260
+ }
261
+ if bytes[cursor] == quote {
262
+ return Some(cursor);
263
+ }
264
+ cursor += 1;
265
+ }
266
+ None
267
+ }
268
+
269
+ fn scan_unquoted_attr_value(bytes: &[u8], mut cursor: usize, limit: usize) -> usize {
270
+ while cursor < limit && !bytes[cursor].is_ascii_whitespace() && bytes[cursor] != b'/' {
271
+ cursor += 1;
272
+ }
273
+ cursor
274
+ }
275
+
276
+ fn is_html_name_byte(byte: u8) -> bool {
277
+ byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'-' | b'_')
278
+ }
279
+
280
+ fn is_html_attr_name_byte(byte: u8) -> bool {
281
+ !byte.is_ascii_whitespace() && !matches!(byte, b'=' | b'/' | b'>')
282
+ }
283
+
284
+ fn skip_ascii_whitespace_until(bytes: &[u8], mut idx: usize, limit: usize) -> usize {
285
+ while idx < limit && matches!(bytes[idx], b' ' | b'\t' | b'\n' | b'\r') {
286
+ idx += 1;
287
+ }
288
+ idx
289
+ }