@nuasite/cms-marker 0.0.80 → 0.0.82
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/dist/types/html-processor.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/manifest-writer.d.ts.map +1 -1
- package/dist/types/seo-processor.d.ts +1 -1
- package/dist/types/seo-processor.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types.d.ts +221 -11
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/html-processor.ts +673 -4
- package/src/index.ts +11 -0
- package/src/manifest-writer.ts +15 -1
- package/src/seo-processor.ts +63 -178
- package/src/types.ts +232 -12
package/src/index.ts
CHANGED
|
@@ -128,8 +128,10 @@ export type { CollectionInfo, MarkdownContent, SourceLocation, VariableReference
|
|
|
128
128
|
// Re-export types for consumers
|
|
129
129
|
export { findCollectionSource, parseMarkdownContent } from './source-finder'
|
|
130
130
|
export type {
|
|
131
|
+
AriaAttributes,
|
|
131
132
|
AvailableColors,
|
|
132
133
|
AvailableTextStyles,
|
|
134
|
+
ButtonAttributes,
|
|
133
135
|
CanonicalUrl,
|
|
134
136
|
CmsManifest,
|
|
135
137
|
CmsMarkerOptions,
|
|
@@ -140,16 +142,24 @@ export type {
|
|
|
140
142
|
ComponentInstance,
|
|
141
143
|
ComponentProp,
|
|
142
144
|
ContentConstraints,
|
|
145
|
+
DataAttributes,
|
|
143
146
|
FieldDefinition,
|
|
144
147
|
FieldType,
|
|
148
|
+
FormAttributes,
|
|
145
149
|
GradientClasses,
|
|
150
|
+
IframeAttributes,
|
|
146
151
|
ImageMetadata,
|
|
152
|
+
InputAttributes,
|
|
147
153
|
JsonLdEntry,
|
|
154
|
+
LinkAttributes,
|
|
148
155
|
ManifestEntry,
|
|
149
156
|
ManifestMetadata,
|
|
157
|
+
MediaAttributes,
|
|
150
158
|
OpacityClasses,
|
|
151
159
|
OpenGraphData,
|
|
160
|
+
PageEntry,
|
|
152
161
|
PageSeoData,
|
|
162
|
+
SelectAttributes,
|
|
153
163
|
SeoKeywords,
|
|
154
164
|
SeoMetaTag,
|
|
155
165
|
SeoOptions,
|
|
@@ -157,6 +167,7 @@ export type {
|
|
|
157
167
|
SeoTitle,
|
|
158
168
|
SourceContext,
|
|
159
169
|
TailwindColor,
|
|
170
|
+
TextareaAttributes,
|
|
160
171
|
TextStyleValue,
|
|
161
172
|
TwitterCardData,
|
|
162
173
|
} from './types'
|
package/src/manifest-writer.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ComponentInstance,
|
|
13
13
|
ManifestEntry,
|
|
14
14
|
ManifestMetadata,
|
|
15
|
+
PageEntry,
|
|
15
16
|
PageSeoData,
|
|
16
17
|
} from './types'
|
|
17
18
|
import { generateManifestContentHash, generateSourceFileHashes } from './utils'
|
|
@@ -209,7 +210,18 @@ export class ManifestWriter {
|
|
|
209
210
|
// Wait for all queued writes to complete
|
|
210
211
|
await this.writeQueue
|
|
211
212
|
|
|
212
|
-
//
|
|
213
|
+
// Build pages array with pathname and title, sorted by pathname
|
|
214
|
+
const pages: PageEntry[] = Array.from(this.pageManifests.entries())
|
|
215
|
+
.map(([pathname, data]) => {
|
|
216
|
+
const entry: PageEntry = { pathname }
|
|
217
|
+
if (data.seo?.title?.content) {
|
|
218
|
+
entry.title = data.seo.title.content
|
|
219
|
+
}
|
|
220
|
+
return entry
|
|
221
|
+
})
|
|
222
|
+
.sort((a, b) => a.pathname.localeCompare(b.pathname))
|
|
223
|
+
|
|
224
|
+
// Write global manifest with settings (component definitions, colors, text styles, collection definitions, and pages)
|
|
213
225
|
if (this.outDir) {
|
|
214
226
|
const globalManifestPath = path.join(this.outDir, this.manifestFile)
|
|
215
227
|
const globalSettings: {
|
|
@@ -217,8 +229,10 @@ export class ManifestWriter {
|
|
|
217
229
|
collectionDefinitions?: Record<string, CollectionDefinition>
|
|
218
230
|
availableColors?: AvailableColors
|
|
219
231
|
availableTextStyles?: AvailableTextStyles
|
|
232
|
+
pages: PageEntry[]
|
|
220
233
|
} = {
|
|
221
234
|
componentDefinitions: this.componentDefinitions,
|
|
235
|
+
pages,
|
|
222
236
|
}
|
|
223
237
|
if (Object.keys(this.collectionDefinitions).length > 0) {
|
|
224
238
|
globalSettings.collectionDefinitions = this.collectionDefinitions
|
package/src/seo-processor.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
1
|
import { type HTMLElement as ParsedHTMLElement, parse } from 'node-html-parser'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { getProjectRoot } from './config'
|
|
5
5
|
import { findSourceLocation } from './source-finder/source-lookup'
|
|
6
|
-
import type { CanonicalUrl, JsonLdEntry, OpenGraphData, PageSeoData,
|
|
6
|
+
import type { CanonicalUrl, JsonLdEntry, OpenGraphData, PageSeoData, SeoKeywords, SeoMetaTag, SeoTitle, TwitterCardData } from './types'
|
|
7
7
|
|
|
8
8
|
/** Type for parsed HTML element nodes from node-html-parser */
|
|
9
9
|
type HTMLNode = ParsedHTMLElement
|
|
@@ -23,7 +23,7 @@ export interface ProcessSeoResult {
|
|
|
23
23
|
/** The modified HTML with title CMS ID if markTitle is enabled */
|
|
24
24
|
html: string
|
|
25
25
|
/** The CMS ID assigned to the title element */
|
|
26
|
-
|
|
26
|
+
titleId?: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -50,29 +50,29 @@ export async function processSeoFromHtml(
|
|
|
50
50
|
|
|
51
51
|
const head = root.querySelector('head')
|
|
52
52
|
const seo: PageSeoData = {}
|
|
53
|
-
let
|
|
53
|
+
let titleId: string | undefined
|
|
54
54
|
|
|
55
55
|
// Extract title
|
|
56
56
|
const titleResult = await extractTitle(root, html, sourcePath, markTitle, getNextId)
|
|
57
57
|
if (titleResult) {
|
|
58
58
|
seo.title = titleResult.title
|
|
59
|
-
|
|
59
|
+
titleId = titleResult.id
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Extract meta tags from head
|
|
63
63
|
if (head) {
|
|
64
|
-
const metaTags = await extractMetaTags(head, html, sourcePath)
|
|
64
|
+
const metaTags = await extractMetaTags(head, html, sourcePath, getNextId)
|
|
65
65
|
categorizeMetaTags(metaTags, seo)
|
|
66
66
|
|
|
67
67
|
// Extract canonical URL
|
|
68
|
-
const canonical = await extractCanonical(head, html, sourcePath)
|
|
68
|
+
const canonical = await extractCanonical(head, html, sourcePath, getNextId)
|
|
69
69
|
if (canonical) {
|
|
70
70
|
seo.canonical = canonical
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Extract JSON-LD
|
|
74
74
|
if (parseJsonLd) {
|
|
75
|
-
const jsonLdEntries = await extractJsonLd(head, html, sourcePath)
|
|
75
|
+
const jsonLdEntries = await extractJsonLd(head, html, sourcePath, getNextId)
|
|
76
76
|
if (jsonLdEntries.length > 0) {
|
|
77
77
|
seo.jsonLd = jsonLdEntries
|
|
78
78
|
}
|
|
@@ -82,7 +82,7 @@ export async function processSeoFromHtml(
|
|
|
82
82
|
return {
|
|
83
83
|
seo,
|
|
84
84
|
html: root.toString(),
|
|
85
|
-
|
|
85
|
+
titleId,
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -95,7 +95,7 @@ async function extractTitle(
|
|
|
95
95
|
sourcePath?: string,
|
|
96
96
|
markTitle?: boolean,
|
|
97
97
|
getNextId?: () => string,
|
|
98
|
-
): Promise<{ title: SeoTitle;
|
|
98
|
+
): Promise<{ title: SeoTitle; id?: string } | undefined> {
|
|
99
99
|
const titleElement = root.querySelector('title')
|
|
100
100
|
if (!titleElement) return undefined
|
|
101
101
|
|
|
@@ -115,19 +115,19 @@ async function extractTitle(
|
|
|
115
115
|
}
|
|
116
116
|
: findElementSourceLocation(titleElement, html, sourcePath)
|
|
117
117
|
|
|
118
|
-
let
|
|
118
|
+
let id: string | undefined
|
|
119
119
|
if (markTitle && getNextId) {
|
|
120
|
-
|
|
121
|
-
titleElement.setAttribute('data-cms-id',
|
|
120
|
+
id = getNextId()
|
|
121
|
+
titleElement.setAttribute('data-cms-id', id)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
return {
|
|
125
125
|
title: {
|
|
126
126
|
content,
|
|
127
|
-
|
|
127
|
+
id,
|
|
128
128
|
...sourceInfo,
|
|
129
129
|
},
|
|
130
|
-
|
|
130
|
+
id,
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -138,6 +138,7 @@ async function extractMetaTags(
|
|
|
138
138
|
head: HTMLNode,
|
|
139
139
|
html: string,
|
|
140
140
|
sourcePath?: string,
|
|
141
|
+
getNextId?: () => string,
|
|
141
142
|
): Promise<SeoMetaTag[]> {
|
|
142
143
|
const metaTags: SeoMetaTag[] = []
|
|
143
144
|
const metas = head.querySelectorAll('meta')
|
|
@@ -150,17 +151,28 @@ async function extractMetaTags(
|
|
|
150
151
|
// Skip meta tags without content or without name/property
|
|
151
152
|
if (!content || (!name && !property)) continue
|
|
152
153
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
// Search for the content attribute value in source files
|
|
158
|
-
const sourceLocation = await findAttributeValueSource('content', content, tagPattern)
|
|
154
|
+
// Use the same source finding logic as regular text entries
|
|
155
|
+
// This tracks through props, variables, and imports
|
|
156
|
+
const sourceLocation = await findSourceLocation(content, 'meta')
|
|
159
157
|
|
|
160
158
|
// Fall back to rendered HTML location if source not found
|
|
161
|
-
const sourceInfo = sourceLocation
|
|
159
|
+
const sourceInfo = sourceLocation
|
|
160
|
+
? {
|
|
161
|
+
sourcePath: sourceLocation.file,
|
|
162
|
+
sourceLine: sourceLocation.line,
|
|
163
|
+
sourceSnippet: sourceLocation.snippet || '',
|
|
164
|
+
}
|
|
165
|
+
: findElementSourceLocation(meta, html, sourcePath)
|
|
166
|
+
|
|
167
|
+
// Mark meta tag with CMS ID for editing
|
|
168
|
+
let id: string | undefined
|
|
169
|
+
if (getNextId) {
|
|
170
|
+
id = getNextId()
|
|
171
|
+
meta.setAttribute('data-cms-id', id)
|
|
172
|
+
}
|
|
162
173
|
|
|
163
174
|
metaTags.push({
|
|
175
|
+
id,
|
|
164
176
|
name: name || undefined,
|
|
165
177
|
property: property || undefined,
|
|
166
178
|
content,
|
|
@@ -172,7 +184,7 @@ async function extractMetaTags(
|
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
/**
|
|
175
|
-
* Categorize meta tags into description, keywords,
|
|
187
|
+
* Categorize meta tags into description, keywords, Open Graph and Twitter Card
|
|
176
188
|
*/
|
|
177
189
|
function categorizeMetaTags(metaTags: SeoMetaTag[], seo: PageSeoData): void {
|
|
178
190
|
const openGraph: OpenGraphData = {}
|
|
@@ -197,16 +209,6 @@ function categorizeMetaTags(metaTags: SeoMetaTag[], seo: PageSeoData): void {
|
|
|
197
209
|
continue
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
// Robots
|
|
201
|
-
if (name === 'robots') {
|
|
202
|
-
const directives = content.split(',').map(d => d.trim().toLowerCase()).filter(Boolean)
|
|
203
|
-
seo.robots = {
|
|
204
|
-
...meta,
|
|
205
|
-
directives,
|
|
206
|
-
} as RobotsDirective
|
|
207
|
-
continue
|
|
208
|
-
}
|
|
209
|
-
|
|
210
212
|
// Open Graph tags
|
|
211
213
|
if (property?.startsWith('og:')) {
|
|
212
214
|
const ogKey = property.replace('og:', '')
|
|
@@ -274,6 +276,7 @@ async function extractCanonical(
|
|
|
274
276
|
head: HTMLNode,
|
|
275
277
|
html: string,
|
|
276
278
|
sourcePath?: string,
|
|
279
|
+
getNextId?: () => string,
|
|
277
280
|
): Promise<CanonicalUrl | undefined> {
|
|
278
281
|
const canonical = head.querySelector('link[rel="canonical"]')
|
|
279
282
|
if (!canonical) return undefined
|
|
@@ -281,13 +284,28 @@ async function extractCanonical(
|
|
|
281
284
|
const href = canonical.getAttribute('href')
|
|
282
285
|
if (!href) return undefined
|
|
283
286
|
|
|
284
|
-
//
|
|
285
|
-
|
|
287
|
+
// Use the same source finding logic as regular text entries
|
|
288
|
+
// This tracks through props, variables, and imports
|
|
289
|
+
const sourceLocation = await findSourceLocation(href, 'link')
|
|
286
290
|
|
|
287
291
|
// Fall back to rendered HTML location if source not found
|
|
288
|
-
const sourceInfo = sourceLocation
|
|
292
|
+
const sourceInfo = sourceLocation
|
|
293
|
+
? {
|
|
294
|
+
sourcePath: sourceLocation.file,
|
|
295
|
+
sourceLine: sourceLocation.line,
|
|
296
|
+
sourceSnippet: sourceLocation.snippet || '',
|
|
297
|
+
}
|
|
298
|
+
: findElementSourceLocation(canonical, html, sourcePath)
|
|
299
|
+
|
|
300
|
+
// Mark canonical link with CMS ID for editing
|
|
301
|
+
let id: string | undefined
|
|
302
|
+
if (getNextId) {
|
|
303
|
+
id = getNextId()
|
|
304
|
+
canonical.setAttribute('data-cms-id', id)
|
|
305
|
+
}
|
|
289
306
|
|
|
290
307
|
return {
|
|
308
|
+
id,
|
|
291
309
|
href,
|
|
292
310
|
...sourceInfo,
|
|
293
311
|
}
|
|
@@ -300,6 +318,7 @@ async function extractJsonLd(
|
|
|
300
318
|
head: HTMLNode,
|
|
301
319
|
html: string,
|
|
302
320
|
sourcePath?: string,
|
|
321
|
+
getNextId?: () => string,
|
|
303
322
|
): Promise<JsonLdEntry[]> {
|
|
304
323
|
const entries: JsonLdEntry[] = []
|
|
305
324
|
|
|
@@ -321,7 +340,15 @@ async function extractJsonLd(
|
|
|
321
340
|
// Fall back to rendered HTML location if source not found
|
|
322
341
|
const sourceInfo = sourceLocation || findElementSourceLocation(script, html, sourcePath)
|
|
323
342
|
|
|
343
|
+
// Mark JSON-LD script with CMS ID for editing
|
|
344
|
+
let id: string | undefined
|
|
345
|
+
if (getNextId) {
|
|
346
|
+
id = getNextId()
|
|
347
|
+
script.setAttribute('data-cms-id', id)
|
|
348
|
+
}
|
|
349
|
+
|
|
324
350
|
entries.push({
|
|
351
|
+
id,
|
|
325
352
|
type,
|
|
326
353
|
data,
|
|
327
354
|
...sourceInfo,
|
|
@@ -469,145 +496,3 @@ function findElementSourceLocation(
|
|
|
469
496
|
sourceSnippet,
|
|
470
497
|
}
|
|
471
498
|
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Search for a text value as an attribute value in source files.
|
|
475
|
-
* Handles both static values (content="text") and dynamic expressions (content={variable}).
|
|
476
|
-
*/
|
|
477
|
-
async function findAttributeValueSource(
|
|
478
|
-
attrName: string,
|
|
479
|
-
value: string,
|
|
480
|
-
tagPattern?: string,
|
|
481
|
-
): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
|
|
482
|
-
const srcDir = path.join(getProjectRoot(), 'src')
|
|
483
|
-
const searchDirs = [
|
|
484
|
-
path.join(srcDir, 'pages'),
|
|
485
|
-
path.join(srcDir, 'layouts'),
|
|
486
|
-
path.join(srcDir, 'components'),
|
|
487
|
-
]
|
|
488
|
-
|
|
489
|
-
for (const dir of searchDirs) {
|
|
490
|
-
try {
|
|
491
|
-
const result = await searchDirForAttributeValue(dir, attrName, value, tagPattern)
|
|
492
|
-
if (result) return result
|
|
493
|
-
} catch {
|
|
494
|
-
// Directory doesn't exist
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return undefined
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Recursively search a directory for attribute values
|
|
503
|
-
*/
|
|
504
|
-
async function searchDirForAttributeValue(
|
|
505
|
-
dir: string,
|
|
506
|
-
attrName: string,
|
|
507
|
-
value: string,
|
|
508
|
-
tagPattern?: string,
|
|
509
|
-
): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
|
|
510
|
-
try {
|
|
511
|
-
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
512
|
-
|
|
513
|
-
for (const entry of entries) {
|
|
514
|
-
const fullPath = path.join(dir, entry.name)
|
|
515
|
-
|
|
516
|
-
if (entry.isDirectory()) {
|
|
517
|
-
const result = await searchDirForAttributeValue(fullPath, attrName, value, tagPattern)
|
|
518
|
-
if (result) return result
|
|
519
|
-
} else if (entry.isFile() && (entry.name.endsWith('.astro') || entry.name.endsWith('.html'))) {
|
|
520
|
-
const result = await searchFileForAttributeValue(fullPath, attrName, value, tagPattern)
|
|
521
|
-
if (result) return result
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
} catch {
|
|
525
|
-
// Error reading directory
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return undefined
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Search a single file for an attribute value
|
|
533
|
-
*/
|
|
534
|
-
async function searchFileForAttributeValue(
|
|
535
|
-
filePath: string,
|
|
536
|
-
attrName: string,
|
|
537
|
-
value: string,
|
|
538
|
-
tagPattern?: string,
|
|
539
|
-
): Promise<{ sourcePath: string; sourceLine: number; sourceSnippet: string } | undefined> {
|
|
540
|
-
try {
|
|
541
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
542
|
-
const lines = content.split('\n')
|
|
543
|
-
|
|
544
|
-
// Escape special regex characters in the value
|
|
545
|
-
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
546
|
-
|
|
547
|
-
// Pattern to match static attribute: attrName="value" or attrName='value'
|
|
548
|
-
const staticPattern = new RegExp(`${attrName}\\s*=\\s*["']${escapedValue}["']`, 'i')
|
|
549
|
-
|
|
550
|
-
// Pattern to match the tag context if provided
|
|
551
|
-
const tagRegex = tagPattern ? new RegExp(tagPattern, 'i') : null
|
|
552
|
-
|
|
553
|
-
for (let i = 0; i < lines.length; i++) {
|
|
554
|
-
const line = lines[i] || ''
|
|
555
|
-
|
|
556
|
-
// Check if this line matches the attribute pattern
|
|
557
|
-
if (staticPattern.test(line)) {
|
|
558
|
-
// If tag pattern provided, verify we're in the right context
|
|
559
|
-
if (tagRegex && !tagRegex.test(line)) {
|
|
560
|
-
// Check surrounding lines for tag context
|
|
561
|
-
const contextLines = lines.slice(Math.max(0, i - 3), i + 1).join(' ')
|
|
562
|
-
if (!tagRegex.test(contextLines)) {
|
|
563
|
-
continue
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Extract the full element snippet
|
|
568
|
-
const snippet = extractElementSnippetFromLines(lines, i, tagPattern)
|
|
569
|
-
|
|
570
|
-
return {
|
|
571
|
-
sourcePath: path.relative(getProjectRoot(), filePath),
|
|
572
|
-
sourceLine: i + 1,
|
|
573
|
-
sourceSnippet: snippet,
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
} catch {
|
|
578
|
-
// Error reading file
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return undefined
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Extract a multi-line element snippet starting from a given line
|
|
586
|
-
*/
|
|
587
|
-
function extractElementSnippetFromLines(lines: string[], startLine: number, tagPattern?: string): string {
|
|
588
|
-
const snippetLines: string[] = []
|
|
589
|
-
|
|
590
|
-
// Look backwards to find the tag opening if we're on an attribute line
|
|
591
|
-
let actualStart = startLine
|
|
592
|
-
for (let i = startLine; i >= Math.max(0, startLine - 5); i--) {
|
|
593
|
-
const line = lines[i] || ''
|
|
594
|
-
if (line.includes('<meta') || line.includes('<link') || line.includes('<title') || line.includes('<script')) {
|
|
595
|
-
actualStart = i
|
|
596
|
-
break
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Collect lines until we find the closing
|
|
601
|
-
for (let i = actualStart; i < Math.min(actualStart + 10, lines.length); i++) {
|
|
602
|
-
const line = lines[i]
|
|
603
|
-
if (!line) continue
|
|
604
|
-
snippetLines.push(line)
|
|
605
|
-
|
|
606
|
-
// Check for self-closing or closing tag
|
|
607
|
-
if (line.includes('/>') || line.includes('</') || (line.includes('>') && !line.includes('<'))) {
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return snippetLines.join('\n')
|
|
613
|
-
}
|