import { Parser } from 'htmlparser2'
import { DefaultDynamicExtractionsRules, DefaultExtractionRules, ExtractionRule } from '../rules'
import { shouldExtract } from '../shouldExtract'
import { ExtractionHTMLOptions } from './options'
import { shiftDetectionPosition } from './utils'
import { DetectionResult } from '~/core/types'
const defaultOptions: Required = {
attributes: ['title', 'alt', 'placeholder', 'label', 'aria-label'],
ignoredTags: ['script', 'style'],
vBind: true,
inlineText: true,
}
export function detect(
input: string,
rules: ExtractionRule[] = DefaultExtractionRules,
dynamicRules: ExtractionRule[] = DefaultDynamicExtractionsRules,
userOptions: ExtractionHTMLOptions = {},
extractScripts?: (script: string, start: number) => DetectionResult[],
): DetectionResult[] {
const {
attributes: ATTRS,
ignoredTags: IGNORED_TAGS,
vBind: V_BIND,
} = Object.assign({}, defaultOptions, userOptions)
const detections: DetectionResult[] = []
// replace svelte inline function, #624
input = input.replace(/<(.*?)={(.*?)}(.*?)>/g, '<$1="$2"$3>')
let lastTag = ''
let lastScriptIndex: number | null = null
const parser = new Parser({
onopentag(name, attrs) {
lastTag = name
if (name === 'script')
lastScriptIndex = parser.endIndex! + 1
if (IGNORED_TAGS.includes(name))
return
const attrNames = Object.keys(attrs).map((name) => {
// static
if (ATTRS.includes(name) && shouldExtract(attrs[name], rules))
return [name, false]
// dynamic
else if (
V_BIND
&& ATTRS.some(n => name === `:${n}` || name === `v-bind:${n}`)
&& shouldExtract(attrs[name], dynamicRules)
)
return [name, true]
return null
})
.filter(Boolean) as [string, boolean][]
if (!attrNames.length)
return
const tagStart = parser.startIndex
const tagEnd = parser.endIndex!
const code = input.slice(tagStart, tagEnd)
for (const [name, isDynamic] of attrNames) {
const match = code.match(
new RegExp(`\\s${name}=(["'])([^\\1]*?)\\1`, 'm'),
)
if (!match)
continue
const fullStart = tagStart + match.index! + 1
const fullEnd = fullStart + match[0].length - 1
const fullText = input.slice(fullStart, fullEnd)
const start = fullStart + name.length + 2 // ="
const end = fullEnd - 1 // "
const text = input.slice(start, end)
detections.push({
text,
start,
end,
isDynamic,
fullStart,
fullEnd,
fullText,
source: 'html-attribute',
})
}
},
onclosetag(name) {
if (name !== 'script' || lastScriptIndex == null)
return
const start = lastScriptIndex
const fullText = input.slice(start, parser.startIndex!)
if (extractScripts)
detections.push(...shiftDetectionPosition(extractScripts(fullText, start), start))
lastScriptIndex = null
},
ontext(fullText) {
if (lastScriptIndex != null)
return
if (IGNORED_TAGS.includes(lastTag))
return
const start = parser.startIndex
const end = parser.endIndex! + 1
const text = fullText.split(/\n/g).map(i => i.trim()).filter(Boolean).join(' ')
if (!shouldExtract(text, rules))
return
detections.push({
text: fullText,
fullText,
start,
end,
source: 'html-inline',
})
},
}, {
xmlMode: true,
lowerCaseTags: false,
lowerCaseAttributeNames: false,
recognizeSelfClosing: true,
})
parser.parseComplete(input)
return detections
}