Skip to content

Commit

Permalink
fix: crash with HTML entities (#278)
Browse files Browse the repository at this point in the history
* fix: crash with HTML entities

* Create empty-bulldogs-trade.md
  • Loading branch information
ota-meshi authored Jan 11, 2024
1 parent 340b420 commit bf924a2
Show file tree
Hide file tree
Showing 9 changed files with 1,896 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-bulldogs-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro-eslint-parser": patch
---

fix: crash with HTML entities
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@typescript-eslint/types": "^5.0.0",
"astrojs-compiler-sync": "^0.3.0",
"debug": "^4.3.4",
"entities": "^4.5.0",
"eslint-visitor-keys": "^3.0.0",
"espree": "^9.0.0",
"semver": "^7.3.8"
Expand Down
104 changes: 94 additions & 10 deletions src/astro/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import type {
ParentNode,
TagLikeNode,
} from "@astrojs/compiler/types";
import {
EntityDecoder,
DecodingMode,
htmlDecodeTree,
} from "entities/lib/decode.js";
import type { Context } from "../context";
import { ParseError } from "../errors";

Expand Down Expand Up @@ -105,7 +110,22 @@ export function calcAttributeEndOffset(
} else if (node.kind === "quoted") {
info = getTokenInfo(
ctx,
[[`"${node.value}"`, `'${node.value}'`, node.value]],
[
[
{
token: `"${node.value}"`,
htmlEntityDecode: true,
},
{
token: `'${node.value}'`,
htmlEntityDecode: true,
},
{
token: node.value,
htmlEntityDecode: true,
},
],
],
calcAttributeValueStartOffset(node, ctx),
);
} else if (node.kind === "expression") {
Expand Down Expand Up @@ -153,7 +173,11 @@ export function calcAttributeValueStartOffset(
if (node.kind === "quoted") {
info = getTokenInfo(
ctx,
[node.name, "=", [`"`, `'`, node.value]],
[
node.name,
"=",
[`"`, `'`, { token: node.value, htmlEntityDecode: true }],
],
node.position!.start.offset,
);
} else if (node.kind === "expression") {
Expand Down Expand Up @@ -340,12 +364,14 @@ function calcExpressionEndOffset(node: ExpressionNode, ctx: Context): number {
return info.index + info.match.length;
}

type TokenParam = { token: string; htmlEntityDecode: boolean };

/**
* Get token info
*/
function getTokenInfo(
ctx: Context,
tokens: (string | string[])[],
tokens: (TokenParam | string | (TokenParam | string)[])[],
position: number,
): {
match: string;
Expand All @@ -361,8 +387,7 @@ function getTokenInfo(
const index = lastMatch
? lastMatch.index + lastMatch.match.length
: position;
const m =
typeof t === "string" ? matchOfStr(t, index) : matchOfForMulti(t, index);
const m = Array.isArray(t) ? matchOfForMulti(t, index) : match(t, index);
if (m == null) {
throw new ParseError(
`Unknown token at ${index}, expected: ${JSON.stringify(
Expand All @@ -377,9 +402,10 @@ function getTokenInfo(
return lastMatch!;

/**
* For string
* For Single Token
*/
function matchOfStr(search: string, position: number) {
function match(token: TokenParam | string, position: number) {
const search = typeof token === "string" ? token : token.token;
const index =
search.trim() === search ? skipSpaces(ctx.code, position) : position;
if (ctx.code.startsWith(search, index)) {
Expand All @@ -388,21 +414,79 @@ function getTokenInfo(
index,
};
}
if (typeof token !== "string") {
return matchWithHTMLEntity(token, index);
}
return null;
}

/**
* For multi
* For Multiple Token
*/
function matchOfForMulti(search: string[], position: number) {
function matchOfForMulti(search: (TokenParam | string)[], position: number) {
for (const s of search) {
const m = matchOfStr(s, position);
const m = match(s, position);
if (m) {
return m;
}
}
return null;
}

/**
* With HTML entity
*/
function matchWithHTMLEntity(token: TokenParam, position: number) {
const search = token.token;
let codeOffset = position;
let searchOffset = 0;
while (searchOffset < search.length) {
const searchChar = search[searchOffset];
if (ctx.code[codeOffset] === searchChar) {
codeOffset++;
searchOffset++;
continue;
}
const entity = getHTMLEntity(codeOffset);
if (entity?.entity === searchChar) {
codeOffset += entity.length;
searchOffset++;
continue;
}
return null;
}
return {
match: ctx.code.slice(position, codeOffset),
index: position,
};

/**
* Get HTML entity from the given position
*/
function getHTMLEntity(position: number) {
let codeOffset = position;
if (ctx.code[codeOffset++] !== "&") return null;

let entity = "";
const entityDecoder = new EntityDecoder(
htmlDecodeTree,
(cp) => (entity += String.fromCodePoint(cp)),
);
entityDecoder.startEntity(DecodingMode.Attribute);
const length = entityDecoder.write(ctx.code, codeOffset);

if (length < 0) {
return null;
}
if (length === 0) {
return null;
}
return {
entity,
length,
};
}
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/parser/ast/html-entity01-input.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
---

<span title="&#x2603;">&copy;</span>
<span title="&copy;">&#x2603;</span>
Loading

0 comments on commit bf924a2

Please sign in to comment.