diff --git a/package.json b/package.json index f5e7f818..20ad18a5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "eslint-fix": "npm run lint -- --fix", "test": "mocha --require ts-node/register \"tests/src/**/*.ts\" --reporter dot --timeout 60000", "cover": "nyc --reporter=lcov npm run test", - "debug": "mocha --require ts-node/register/transpile-only \"tests/src/**/*.ts\" --reporter dot", + "debug": "mocha --require ts-node/register/transpile-only \"tests/src/**/*.ts\" --reporter dot --timeout 60000", "preversion": "npm run lint && npm test", "update-fixtures": "ts-node --transpile-only ./tools/update-fixtures.ts", "eslint-playground": "eslint tests/fixtures --ext .svelte --config .eslintrc-for-playground.js --format codeframe", @@ -52,6 +52,7 @@ "@ota-meshi/eslint-plugin": "^0.10.0", "@ota-meshi/eslint-plugin-svelte": "^0.22.0", "@types/benchmark": "^2.1.1", + "@types/chai": "^4.3.0", "@types/eslint": "^8.0.0", "@types/eslint-scope": "^3.7.0", "@types/eslint-visitor-keys": "^1.0.0", @@ -61,6 +62,7 @@ "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "benchmark": "^2.1.4", + "chai": "^4.3.4", "code-red": "^0.2.3", "eslint": "^8.2.0", "eslint-config-prettier": "^8.3.0", @@ -77,6 +79,7 @@ "locate-character": "^2.0.5", "magic-string": "^0.25.7", "mocha": "^9.1.3", + "mocha-chai-jest-snapshot": "^1.1.3", "nyc": "^15.1.0", "prettier": "^2.0.5", "prettier-plugin-svelte": "^2.5.0", diff --git a/src/context/index.ts b/src/context/index.ts index d6d23c4e..360044a4 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -1,10 +1,19 @@ import fs from "fs" import path from "path" -import type { Comment, Locations, Position, Token } from "../ast" +import type { + Comment, + Locations, + Position, + SvelteScriptElement, + SvelteStyleElement, + Token, +} from "../ast" import type ESTree from "estree" import { ScriptLetContext } from "./script-let" import { LetDirectiveCollections } from "./let-directive-collection" import { getParserName } from "../parser/resolve-parser" +import type { AttributeToken } from "../parser/html" +import { parseAttributes } from "../parser/html" export class ScriptsSourceCode { private raw: string @@ -87,6 +96,8 @@ export class Context { private state: { isTypeScript?: boolean } = {} + private readonly blocks: Block[] = [] + public constructor(code: string, parserOptions: any) { this.code = code this.parserOptions = parserOptions @@ -96,21 +107,25 @@ export class Context { let templateCode = "" let scriptCode = "" - let scriptAttrs: Record = {} + const scriptAttrs: Record = {} let start = 0 for (const block of extractBlocks(code)) { + this.blocks.push(block) templateCode += - code.slice(start, block.codeRange[0]) + - spaces.slice(block.codeRange[0], block.codeRange[1]) + code.slice(start, block.contentRange[0]) + + spaces.slice(block.contentRange[0], block.contentRange[1]) if (block.tag === "script") { scriptCode += - spaces.slice(start, block.codeRange[0]) + block.code - scriptAttrs = Object.assign(scriptAttrs, block.attrs) + spaces.slice(start, block.contentRange[0]) + + code.slice(...block.contentRange) + for (const attr of block.attrs) { + scriptAttrs[attr.key.name] = attr.value?.value + } } else { - scriptCode += spaces.slice(start, block.codeRange[1]) + scriptCode += spaces.slice(start, block.contentRange[1]) } - start = block.codeRange[1] + start = block.contentRange[1] } templateCode += code.slice(start) scriptCode += spaces.slice(start) @@ -223,49 +238,63 @@ export class Context { public stripScriptCode(start: number, end: number): void { this.sourceCode.scripts.stripCode(start, end) } + + public findBlock( + element: SvelteScriptElement | SvelteStyleElement, + ): Block | undefined { + const tag = element.type === "SvelteScriptElement" ? "script" : "style" + return this.blocks.find( + (block) => + block.tag === tag && + element.range[0] <= block.contentRange[0] && + block.contentRange[1] <= element.range[1], + ) + } } -/** Extract " : "" - let re - let index = 0 - while ((re = startRegex.exec(elementCode))) { - const [, attributes] = re - - const endTagIndex = elementCode.indexOf(endTag, startRegex.lastIndex) - if (endTagIndex >= 0) { - const contextLength = endTagIndex - startRegex.lastIndex - code += elementCode.slice(index, re.index) - code += `${script ? "
` - code += `${" ".repeat(contextLength)}
` - startRegex.lastIndex = index = endTagIndex + endTag.length - } else { - break - } - } - code += elementCode.slice(index) - const svelteAst = parse(code) as SvAST.Ast - - const fakeElement = svelteAst.html.children.find( - (c) => c.type === "Element", - ) as SvAST.Element - element.startTag = { type: "SvelteStartTag", attributes: [], @@ -182,7 +150,42 @@ function extractAttributes( end: null as any, }, } - element.startTag.attributes.push( - ...convertAttributes(fakeElement.attributes, element.startTag, ctx), - ) + const block = ctx.findBlock(element) + if (block) { + for (const attr of block.attrs) { + const attrNode: SvelteAttribute = { + type: "SvelteAttribute", + boolean: false, + key: null as any, + value: [], + parent: element.startTag, + ...ctx.getConvertLocation({ + start: attr.key.start, + end: attr.value?.end ?? attr.key.end, + }), + } + element.startTag.attributes.push(attrNode) + attrNode.key = { + type: "SvelteName", + name: attr.key.name, + parent: attrNode, + ...ctx.getConvertLocation(attr.key), + } + ctx.addToken("HTMLIdentifier", attr.key) + if (attr.value == null) { + attrNode.boolean = true + } else { + const valueLoc = attr.value.quote + ? { start: attr.value.start + 1, end: attr.value.end - 1 } + : attr.value + attrNode.value.push({ + type: "SvelteLiteral", + value: attr.value.value, + parent: attrNode, + ...ctx.getConvertLocation(valueLoc), + }) + extractTextTokens(valueLoc, ctx) + } + } + } } diff --git a/src/parser/converts/text.ts b/src/parser/converts/text.ts index 82674660..bdda6f8d 100644 --- a/src/parser/converts/text.ts +++ b/src/parser/converts/text.ts @@ -46,16 +46,16 @@ export function convertTemplateLiteralToLiteral( parent, ...ctx.getConvertLocation(node), } - extractTextTokens(node, ctx) + extractTextTokens(getWithLoc(node), ctx) return text } /** Extract tokens */ -function extractTextTokens( - node: SvAST.Text | ESTree.TemplateLiteral, +export function extractTextTokens( + node: { start: number; end: number }, ctx: Context, -) { - const loc = getWithLoc(node) +): void { + const loc = node let start = loc.start let word = false for (let index = loc.start; index < loc.end; index++) { diff --git a/src/parser/html.ts b/src/parser/html.ts new file mode 100644 index 00000000..510bd4d8 --- /dev/null +++ b/src/parser/html.ts @@ -0,0 +1,190 @@ +export type AttributeToken = { + key: AttributeKeyToken + value: AttributeValueToken | null +} +export type AttributeKeyToken = { + name: string + start: number + end: number +} +export type AttributeValueToken = { + value: string + quote: '"' | "'" | null + start: number + end: number +} + +const spacePattern = /\s/ + +/** Parse HTML attributes */ +export function parseAttributes( + code: string, + startIndex: number, +): { attributes: AttributeToken[]; index: number } { + const attributes: AttributeToken[] = [] + + let index = startIndex + while (index < code.length) { + const char = code[index] + if (spacePattern.test(char)) { + index++ + continue + } + if (char === ">") break + if (char === "/" && code[index + 1] === ">") break + const attrData = parseAttribute(code, index) + attributes.push(attrData.attribute) + index = attrData.index + } + + return { attributes, index } +} + +/** Parse HTML attribute */ +function parseAttribute( + code: string, + startIndex: number, +): { attribute: AttributeToken; index: number } { + // parse key + const keyData = parseAttributeKey(code, startIndex) + const key = keyData.key + let index = keyData.index + if (code[index] !== "=") { + return { + attribute: { + key, + value: null, + }, + index, + } + } + + index++ + + // skip spaces + while (index < code.length) { + const char = code[index] + if (spacePattern.test(char)) { + index++ + continue + } + break + } + + // parse value + const valueData = parseAttributeValue(code, index) + + return { + attribute: { + key, + value: valueData.value, + }, + index: valueData.index, + } +} + +/** Parse HTML attribute key */ +function parseAttributeKey( + code: string, + startIndex: number, +): { key: AttributeKeyToken; index: number } { + const key: AttributeKeyToken = { + name: code[startIndex], + start: startIndex, + end: startIndex + 1, + } + let index = key.end + while (index < code.length) { + const char = code[index] + if (char === "=") { + break + } + if (spacePattern.test(char)) { + for (let i = index; i < code.length; i++) { + const c = code[i] + if (c === "=") { + return { + key, + index: i, + } + } + if (spacePattern.test(c)) { + continue + } + return { + key, + index, + } + } + break + } + key.name += char + index++ + key.end = index + } + return { + key, + index, + } +} + +/** Parse HTML attribute value */ +function parseAttributeValue( + code: string, + startIndex: number, +): { value: AttributeValueToken | null; index: number } { + let index = startIndex + const maybeQuote = code[index] + if (maybeQuote == null) { + return { + value: null, + index, + } + } + const quote = maybeQuote === '"' || maybeQuote === "'" ? maybeQuote : null + if (quote) { + index++ + } + const valueFirstChar = code[index] + if (valueFirstChar == null) { + return { + value: { + value: maybeQuote, + quote: null, + start: startIndex, + end: index, + }, + index, + } + } + const value: AttributeValueToken = { + value: valueFirstChar, + quote, + start: startIndex, + end: index + 1, + } + index = value.end + while (index < code.length) { + const char = code[index] + if (quote) { + if (quote === char) { + index++ + value.end = index + break + } + } else if ( + spacePattern.test(char) || + char === ">" || + (char === "/" && code[index + 1] === ">") + ) { + break + } + value.value += char + index++ + value.end = index + } + return { + value, + index, + } +} diff --git a/tests/src/parser/__snapshots__/html.ts.snap b/tests/src/parser/__snapshots__/html.ts.snap new file mode 100644 index 00000000..40933937 --- /dev/null +++ b/tests/src/parser/__snapshots__/html.ts.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseAttributes