UNPKG

prettier-plugin-blade

Version:

Prettier plugin for Laravel Blade templates

1 lines 1.1 MB
{"version":3,"sources":["../src/index.ts","../src/lexer/directives.ts","../src/frontend-attribute-names.ts","../src/lexer/scan-primitives.ts","../src/lexer/lexer.ts","../src/ignore-markers.ts","../src/lexer/ignore-ranges.ts","../src/tree/types.ts","../src/tree/void-elements.ts","../src/tree/optional-tags.ts","../src/tree/construct-scanner.ts","../src/tree/directives.ts","../src/tree/directive-helper.ts","../src/tree/directive-token-index.ts","../src/tree/argument-scanner.ts","../src/rawtext-script-scanner.ts","../src/malformed-tags.ts","../src/tree/tree-builder.ts","../src/pragma.ts","../src/plugins/sage/metadata.ts","../src/plugins/types.ts","../src/plugins/sage/print.ts","../src/plugins/sage/index.ts","../src/plugins/statamic.ts","../src/plugins/runtime.ts","../src/front-matter.ts","../src/line-offsets.ts","../src/parser.ts","../src/print/index.ts","../src/html-whitespace.ts","../src/preprocess/pipeline.ts","../src/preprocess/whitespace-model.ts","../src/constants.ts","../src/node-predicates.ts","../src/preprocess.ts","../src/html-data.ts","../src/print/style-at-rules.ts","../src/print/blade-syntax.ts","../src/print/utils.ts","../src/print/element.ts","../src/print/blade-options.ts","../src/print/tag.ts","../src/print/attribute-name.ts","../src/print/doctype-utils.ts","../src/print/doc-utils.ts","../src/print/children.ts","../src/print/if-break-chain.ts","../src/print/text.ts","../src/print/directive.ts","../src/print/directive-spacing-context.ts","../src/string-utils.ts","../src/print/echo.ts","../src/print/echo-normalization.ts","../src/print/comment.ts","../src/print/doctype.ts","../src/print/embed.ts","../src/print/embed/attribute.ts","../src/print/embed/utilities.ts","../src/print/embed/alpine-attributes.ts","../src/print/embed/tailwind.ts","../src/print/embed/embedded-parser-plugins.ts","../src/print/embed/php-plugin.ts","../src/print/embed/php.ts","../src/print/embed/raw-content.ts"],"sourcesContent":["import type { Plugin, SupportOption } from \"prettier\";\nimport { bladeParser } from \"./parser.js\";\nimport { bladePrinter } from \"./printer.js\";\nimport { DEFAULT_DIRECTIVE_ARG_SPACING_OVERRIDE_TOKENS } from \"./print/blade-options.js\";\n\nconst languages: Plugin[\"languages\"] = [\n {\n name: \"blade\",\n parsers: [\"blade\"],\n extensions: [\".blade.php\"],\n vscodeLanguageIds: [\"blade\"],\n },\n];\n\nconst parsers: Plugin[\"parsers\"] = {\n blade: bladeParser,\n};\n\nconst printers: Plugin[\"printers\"] = {\n \"blade-ast\": bladePrinter,\n};\n\n/**\n * Declare HTML-specific options so Prettier passes them through.\n */\nconst options: Record<string, SupportOption> = {\n htmlWhitespaceSensitivity: {\n category: \"HTML\",\n type: \"choice\",\n default: \"css\",\n description: \"How to handle whitespaces in HTML.\",\n choices: [\n {\n value: \"css\",\n description: \"Respect the default value of CSS display property.\",\n },\n {\n value: \"strict\",\n description: \"Whitespaces are considered sensitive.\",\n },\n {\n value: \"ignore\",\n description: \"Whitespaces are considered insensitive.\",\n },\n ],\n },\n bladePhpFormatting: {\n category: \"Blade\",\n type: \"choice\",\n default: \"safe\",\n description: \"Format Blade PHP fragments (directive args, echoes, and PHP blocks/tags).\",\n choices: [\n {\n value: \"off\",\n description: \"Disable Blade PHP fragment formatting.\",\n },\n {\n value: \"safe\",\n description: \"Format known-safe Blade PHP fragments with conservative wrappers.\",\n },\n {\n value: \"aggressive\",\n description: \"Try additional wrapper strategies before falling back to original text.\",\n },\n ],\n },\n bladePhpFormattingTargets: {\n category: \"Blade\",\n type: \"string\",\n array: true,\n default: [{ value: [\"directiveArgs\", \"echo\", \"phpBlock\", \"phpTag\"] }],\n description:\n \"PHP embedding targets: echo, directiveArgs, phpBlock, phpTag. Use [] (or CLI value 'none') to disable all targets.\",\n },\n bladeSyntaxPlugins: {\n category: \"Blade\",\n type: \"string\",\n array: true,\n default: [{ value: [\"statamic\"] }],\n description: \"List of Blade syntax plugins, e.g. statamic or log1x/sage-directives.\",\n },\n bladeDirectiveCase: {\n category: \"Blade\",\n type: \"choice\",\n default: \"preserve\",\n description: \"Normalize Blade directive casing.\",\n choices: [\n {\n value: \"preserve\",\n description: \"Keep directive case as written.\",\n },\n {\n value: \"canonical\",\n description: \"Use canonical directive casing for known directives.\",\n },\n {\n value: \"lower\",\n description: \"Lowercase all directive names.\",\n },\n ],\n },\n bladeDirectiveCaseMap: {\n category: \"Blade\",\n type: \"string\",\n default: \"\",\n description: 'JSON object mapping directive names to canonical case, e.g. {\"disk\":\"Disk\"}.',\n },\n bladeDirectiveArgSpacing: {\n category: \"Blade\",\n type: \"choice\",\n default: \"space\",\n description: \"Spacing between directive name and argument list.\",\n choices: [\n {\n value: \"preserve\",\n description: \"Preserve original spacing.\",\n },\n {\n value: \"none\",\n description: \"Print without a space, e.g. @if($x).\",\n },\n {\n value: \"space\",\n description: \"Print with one space, e.g. @if ($x).\",\n },\n ],\n },\n bladeDirectiveArgSpacingOverrides: {\n category: \"Blade\",\n type: \"string\",\n array: true,\n default: [{ value: [...DEFAULT_DIRECTIVE_ARG_SPACING_OVERRIDE_TOKENS] }],\n description:\n \"Directive spacing overrides in directive[=rule] form; bare directive names imply space.\",\n },\n bladeDirectiveBlockStyle: {\n category: \"Blade\",\n type: \"choice\",\n default: \"preserve\",\n description: \"Formatting style for directive blocks.\",\n choices: [\n {\n value: \"preserve\",\n description: \"Preserve inline block intent when written on one line.\",\n },\n {\n value: \"inline-if-short\",\n description: \"Allow short blocks to print inline when possible.\",\n },\n {\n value: \"multiline\",\n description: \"Always print directive blocks in multiline style.\",\n },\n ],\n },\n bladeBlankLinesAroundDirectives: {\n category: \"Blade\",\n type: \"choice\",\n default: \"preserve\",\n description: \"Blank line policy between directives inside a block.\",\n choices: [\n {\n value: \"preserve\",\n description: \"Preserve existing blank line intent.\",\n },\n {\n value: \"always\",\n description: \"Insert a blank line between directive segments.\",\n },\n ],\n },\n bladeEchoSpacing: {\n category: \"Blade\",\n type: \"choice\",\n default: \"preserve\",\n description: \"Spacing style for Blade echo delimiters.\",\n choices: [\n {\n value: \"preserve\",\n description: \"Preserve current echo spacing.\",\n },\n {\n value: \"space\",\n description: \"Use spaced delimiters, e.g. {{ $x }}.\",\n },\n {\n value: \"tight\",\n description: \"Use tight delimiters, e.g. {{$x}}.\",\n },\n ],\n },\n bladeSlotClosingTag: {\n category: \"Blade\",\n type: \"choice\",\n default: \"canonical\",\n description: \"How to print Blade slot closing tags for shorthand pairs.\",\n choices: [\n {\n value: \"canonical\",\n description: \"Use canonical closing tag names based on the opening slot tag.\",\n },\n {\n value: \"preserve\",\n description: \"Preserve shorthand closing tags such as </x-slot> when present.\",\n },\n ],\n },\n bladeVoidElementSlash: {\n category: \"Blade\",\n type: \"choice\",\n default: \"always\",\n description: \"How to print the self-closing slash on standard HTML void elements.\",\n choices: [\n {\n value: \"always\",\n description: \"Always print standard HTML void elements with a slash, e.g. <meta />.\",\n },\n {\n value: \"never\",\n description: \"Never print a slash on standard HTML void elements, e.g. <meta>.\",\n },\n {\n value: \"preserve\",\n description: \"Preserve whether standard HTML void elements used a source slash.\",\n },\n ],\n },\n bladeInlineIntentElements: {\n category: \"Blade\",\n type: \"string\",\n array: true,\n default: [{ value: [\"p\", \"svg\", \"svg:*\"] }],\n description:\n \"Element names that should preserve single-line inline intent and ignore printWidth-driven wrapping when source is inline. Supports namespace wildcards like svg:*.\",\n },\n bladeComponentPrefixes: {\n category: \"Blade\",\n type: \"string\",\n array: true,\n default: [{ value: [\"x\", \"s\", \"statamic\", \"flux\", \"livewire\", \"native\"] }],\n description:\n \"Component prefixes used to detect Blade components for :bound attribute PHP formatting.\",\n },\n bladeInsertOptionalClosingTags: {\n category: \"Blade\",\n type: \"boolean\",\n default: false,\n description:\n \"Insert explicit closing tags when elements are implicitly closed in source (optional-end-tag and parser-recovered missing closing tags).\",\n },\n bladeKeepHeadAndBodyAtRoot: {\n category: \"Blade\",\n type: \"boolean\",\n default: true,\n description:\n \"Keep root-level <head> and <body> tags flush with <html> for canonical HTML documents.\",\n },\n};\n\nconst plugin: Plugin = { languages, parsers, printers, options };\n\nexport default plugin;\nexport { languages, parsers, printers, options };\n","const DEFAULT_DIRECTIVES: string[] = [\r\n \"if\",\r\n \"elseif\",\r\n \"else\",\r\n \"endif\",\r\n \"unless\",\r\n \"endunless\",\r\n \"isset\",\r\n \"endisset\",\r\n \"empty\",\r\n \"endempty\",\r\n \"switch\",\r\n \"case\",\r\n \"break\",\r\n \"default\",\r\n \"endswitch\",\r\n\r\n \"foreach\",\r\n \"endforeach\",\r\n \"for\",\r\n \"endfor\",\r\n \"while\",\r\n \"endwhile\",\r\n \"forelse\",\r\n \"endforelse\",\r\n \"continue\",\r\n\r\n \"auth\",\r\n \"elseauth\",\r\n \"endauth\",\r\n \"guest\",\r\n \"elseguest\",\r\n \"endguest\",\r\n\r\n \"can\",\r\n \"elsecan\",\r\n \"endcan\",\r\n \"canany\",\r\n \"elsecanany\",\r\n \"endcanany\",\r\n \"cannot\",\r\n \"elsecannot\",\r\n \"endcannot\",\r\n\r\n \"env\",\r\n \"elseenv\",\r\n \"endenv\",\r\n \"production\",\r\n \"elseproduction\",\r\n \"endproduction\",\r\n\r\n \"section\",\r\n \"endsection\",\r\n \"yield\",\r\n \"show\",\r\n \"stop\",\r\n \"append\",\r\n \"overwrite\",\r\n \"extends\",\r\n \"extendsFirst\",\r\n \"parent\",\r\n \"hasSection\",\r\n \"sectionMissing\",\r\n \"endhasSection\",\r\n \"endsectionMissing\",\r\n\r\n \"include\",\r\n \"includeIf\",\r\n \"includeWhen\",\r\n \"includeUnless\",\r\n \"includeFirst\",\r\n \"includeIsolated\",\r\n \"each\",\r\n\r\n \"once\",\r\n \"endonce\",\r\n\r\n \"push\",\r\n \"endpush\",\r\n \"pushOnce\",\r\n \"endPushOnce\",\r\n \"pushIf\",\r\n \"elsePushIf\",\r\n \"elsePush\",\r\n \"endPushIf\",\r\n \"prepend\",\r\n \"endprepend\",\r\n \"prependOnce\",\r\n \"endPrependOnce\",\r\n \"stack\",\r\n \"hasStack\",\r\n\r\n \"component\",\r\n \"endcomponent\",\r\n \"endComponentClass\",\r\n \"componentFirst\",\r\n \"endComponentFirst\",\r\n \"slot\",\r\n \"endslot\",\r\n \"props\",\r\n \"aware\",\r\n\r\n \"csrf\",\r\n \"method\",\r\n \"error\",\r\n \"enderror\",\r\n \"old\",\r\n\r\n \"inject\",\r\n \"dd\",\r\n \"dump\",\r\n \"vite\",\r\n \"viteReactRefresh\",\r\n \"fonts\",\r\n \"json\",\r\n \"js\",\r\n \"unset\",\r\n\r\n \"class\",\r\n \"style\",\r\n \"checked\",\r\n \"selected\",\r\n \"disabled\",\r\n \"readonly\",\r\n \"required\",\r\n \"bool\",\r\n\r\n \"php\",\r\n \"endphp\",\r\n \"verbatim\",\r\n \"endverbatim\",\r\n\r\n \"fragment\",\r\n \"endfragment\",\r\n\r\n \"session\",\r\n \"endsession\",\r\n\r\n \"context\",\r\n \"endcontext\",\r\n\r\n \"lang\",\r\n \"endlang\",\r\n \"choice\",\r\n\r\n \"livewire\",\r\n \"livewireStyles\",\r\n \"livewireScripts\",\r\n \"entangle\",\r\n \"this\",\r\n \"persist\",\r\n \"endpersist\",\r\n \"teleport\",\r\n \"endteleport\",\r\n \"volt\",\r\n\r\n // Inertia.js\r\n \"inertia\",\r\n \"inertiaHead\",\r\n // Filament\r\n \"filamentStyles\",\r\n \"filamentScripts\",\r\n // Blade Icons\r\n \"svg\",\r\n // Spatie Permission\r\n \"role\",\r\n \"endrole\",\r\n \"hasrole\",\r\n \"endhasrole\",\r\n \"hasanyrole\",\r\n \"endhasanyrole\",\r\n \"hasallroles\",\r\n \"endhasallroles\",\r\n \"unlessrole\",\r\n \"endunlessrole\",\r\n // Pennant\r\n \"feature\",\r\n \"endfeature\",\r\n \"featureany\",\r\n \"endfeatureany\",\r\n // Cashier Paddle\r\n \"paddleJS\",\r\n\r\n \"use\",\r\n];\r\n\r\nconst CANONICAL_DIRECTIVE_CASE = new Map<string, string>();\r\nfor (const name of DEFAULT_DIRECTIVES) {\r\n const lower = name.toLowerCase();\r\n if (!CANONICAL_DIRECTIVE_CASE.has(lower)) {\r\n CANONICAL_DIRECTIVE_CASE.set(lower, name);\r\n }\r\n}\r\n\r\nexport type DirectivePhpWrapperKind =\r\n | \"for\"\r\n | \"foreach\"\r\n | \"while\"\r\n | \"switch\"\r\n | \"case\"\r\n | \"if\"\r\n | \"call\";\r\n\r\nexport type DirectivePhpWrapperMode = \"safe\" | \"aggressive\";\r\n\r\nexport interface DirectivePhpWrapperContext {\r\n hasDirective?: (name: string) => boolean;\r\n isConditionLikeDirective?: (name: string) => boolean;\r\n}\r\n\r\nconst IF_WRAPPER_DIRECTIVES = new Set([\r\n \"if\",\r\n \"elseif\",\r\n \"unless\",\r\n \"isset\",\r\n \"once\",\r\n \"auth\",\r\n \"elseauth\",\r\n \"guest\",\r\n \"elseguest\",\r\n \"can\",\r\n \"elsecan\",\r\n \"cannot\",\r\n \"elsecannot\",\r\n \"canany\",\r\n \"elsecanany\",\r\n \"env\",\r\n \"elseenv\",\r\n \"production\",\r\n \"elseproduction\",\r\n \"hassection\",\r\n \"sectionmissing\",\r\n \"error\",\r\n \"role\",\r\n \"hasrole\",\r\n \"hasanyrole\",\r\n \"hasallroles\",\r\n \"unlessrole\",\r\n \"pushif\",\r\n \"elsepushif\",\r\n \"hasstack\",\r\n]);\r\n\r\nconst AGGRESSIVE_WRAPPER_ORDER: DirectivePhpWrapperKind[] = [\r\n \"if\",\r\n \"while\",\r\n \"switch\",\r\n \"foreach\",\r\n \"for\",\r\n \"case\",\r\n \"call\",\r\n];\r\n\r\nexport function getDirectivePhpWrapperKind(\r\n directiveName: string,\r\n context: DirectivePhpWrapperContext = {},\r\n): DirectivePhpWrapperKind {\r\n const name = directiveName.toLowerCase();\r\n const hasDirective = context.hasDirective;\r\n\r\n if (context.isConditionLikeDirective?.(name)) {\r\n return \"if\";\r\n }\r\n\r\n if (name === \"for\") return \"for\";\r\n if (name === \"foreach\" || name === \"forelse\") return \"foreach\";\r\n if (name === \"while\") return \"while\";\r\n if (name === \"switch\") return \"switch\";\r\n if (name === \"case\") return \"case\";\r\n if (IF_WRAPPER_DIRECTIVES.has(name)) return \"if\";\r\n\r\n // Custom condition-like opener detected by training:\r\n // @disk ... @elsedisk ... @enddisk\r\n if (hasDirective?.(`else${name}`) && hasDirective(`end${name}`)) {\r\n return \"if\";\r\n }\r\n\r\n // Custom condition-like branch name itself (e.g. @elsedisk).\r\n if (name.startsWith(\"else\") && name.length > 4) {\r\n const baseName = name.slice(4);\r\n if (hasDirective?.(baseName) && hasDirective(`end${baseName}`)) {\r\n return \"if\";\r\n }\r\n }\r\n\r\n return \"call\";\r\n}\r\n\r\nexport function getDirectivePhpWrapperKinds(\r\n directiveName: string,\r\n mode: DirectivePhpWrapperMode,\r\n context: DirectivePhpWrapperContext = {},\r\n): DirectivePhpWrapperKind[] {\r\n const first = getDirectivePhpWrapperKind(directiveName, context);\r\n\r\n if (mode === \"safe\") {\r\n return first === \"call\" ? [\"call\"] : [first, \"call\"];\r\n }\r\n\r\n const kinds: DirectivePhpWrapperKind[] = [first];\r\n for (const kind of AGGRESSIVE_WRAPPER_ORDER) {\r\n if (!kinds.includes(kind)) {\r\n kinds.push(kind);\r\n }\r\n }\r\n\r\n return kinds;\r\n}\r\n\r\nexport function getCanonicalDirectiveName(directiveName: string): string | null {\r\n return CANONICAL_DIRECTIVE_CASE.get(directiveName.toLowerCase()) ?? null;\r\n}\r\n\r\nexport class Directives {\r\n private known: Set<string>;\r\n private _acceptAll: boolean;\r\n\r\n private constructor(known: Set<string>, acceptAll: boolean) {\r\n this.known = known;\r\n this._acceptAll = acceptAll;\r\n }\r\n\r\n static acceptAll(): Directives {\r\n return new Directives(new Set(), true);\r\n }\r\n\r\n static withDefaults(extraNames: Iterable<string> = []): Directives {\r\n const set = new Set<string>();\r\n for (const name of DEFAULT_DIRECTIVES) {\r\n set.add(name.toLowerCase());\r\n }\r\n for (const name of extraNames) {\r\n const normalized = name.trim().toLowerCase().replace(/^@/, \"\");\r\n if (normalized) set.add(normalized);\r\n }\r\n return new Directives(set, false);\r\n }\r\n\r\n static empty(): Directives {\r\n return new Directives(new Set(), false);\r\n }\r\n\r\n static from(names: Iterable<string>): Directives {\r\n const set = new Set<string>();\r\n for (const name of names) {\r\n set.add(name.toLowerCase());\r\n }\r\n return new Directives(set, false);\r\n }\r\n\r\n isDirective(name: string): boolean {\r\n if (this._acceptAll) return true;\r\n return this.known.has(name.toLowerCase());\r\n }\r\n\r\n isDirectiveLower(nameLower: string): boolean {\r\n if (this._acceptAll) return true;\r\n return this.known.has(nameLower);\r\n }\r\n\r\n acceptsAll(): boolean {\r\n return this._acceptAll;\r\n }\r\n\r\n register(name: string): this {\r\n this.known.add(name.toLowerCase());\r\n return this;\r\n }\r\n}\r\n","const DOM_EVENT_NAMES = new Set([\n \"abort\",\n \"animationend\",\n \"animationiteration\",\n \"animationstart\",\n \"auxclick\",\n \"beforeinput\",\n \"beforematch\",\n \"beforetoggle\",\n \"beforeunload\",\n \"blur\",\n \"cancel\",\n \"canplay\",\n \"canplaythrough\",\n \"change\",\n \"click\",\n \"close\",\n \"contentvisibilityautostatechange\",\n \"contextlost\",\n \"contextmenu\",\n \"contextrestored\",\n \"copy\",\n \"cuechange\",\n \"cut\",\n \"dblclick\",\n \"drag\",\n \"dragend\",\n \"dragenter\",\n \"dragleave\",\n \"dragover\",\n \"dragstart\",\n \"drop\",\n \"durationchange\",\n \"emptied\",\n \"ended\",\n \"error\",\n \"focus\",\n \"focusin\",\n \"focusout\",\n \"formdata\",\n \"gotpointercapture\",\n \"input\",\n \"invalid\",\n \"keydown\",\n \"keypress\",\n \"keyup\",\n \"load\",\n \"loadeddata\",\n \"loadedmetadata\",\n \"loadstart\",\n \"lostpointercapture\",\n \"mousedown\",\n \"mouseenter\",\n \"mouseleave\",\n \"mousemove\",\n \"mouseout\",\n \"mouseover\",\n \"mouseup\",\n \"paste\",\n \"pause\",\n \"play\",\n \"playing\",\n \"pointercancel\",\n \"pointerdown\",\n \"pointerenter\",\n \"pointerleave\",\n \"pointermove\",\n \"pointerout\",\n \"pointerover\",\n \"pointerrawupdate\",\n \"pointerup\",\n \"progress\",\n \"ratechange\",\n \"reset\",\n \"resize\",\n \"scroll\",\n \"scrollend\",\n \"securitypolicyviolation\",\n \"seeked\",\n \"seeking\",\n \"select\",\n \"selectionchange\",\n \"selectstart\",\n \"slotchange\",\n \"stalled\",\n \"submit\",\n \"suspend\",\n \"timeupdate\",\n \"toggle\",\n \"touchcancel\",\n \"touchend\",\n \"touchmove\",\n \"touchstart\",\n \"transitioncancel\",\n \"transitionend\",\n \"transitionrun\",\n \"transitionstart\",\n \"unload\",\n \"volumechange\",\n \"waiting\",\n \"webkitanimationend\",\n \"webkitanimationiteration\",\n \"webkitanimationstart\",\n \"webkittransitionend\",\n \"wheel\",\n]);\n\nexport function isFrontendEventStyleAtName(name: string): boolean {\n return DOM_EVENT_NAMES.has(name.toLowerCase());\n}\n\nexport function isHtmlEventAttribute(name: string): boolean {\n const lower = name.toLowerCase();\n if (!lower.startsWith(\"on\")) return false;\n return DOM_EVENT_NAMES.has(lower.slice(2));\n}\n","export interface PosRef {\n value: number;\n}\n\nexport function isAsciiAlpha(ch: number): boolean {\n return (ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122);\n}\n\nexport function isAsciiDigit(ch: number): boolean {\n return ch >= 48 && ch <= 57;\n}\n\nexport function isAsciiAlnum(ch: number): boolean {\n return isAsciiAlpha(ch) || isAsciiDigit(ch);\n}\n\nexport function canStartBladeDirectiveAt(source: string, pos: number, boundaryStart = 0): boolean {\n if (pos <= boundaryStart) return pos === boundaryStart;\n const prev = source.charCodeAt(pos - 1);\n return !isAsciiAlnum(prev) && prev !== 64 /* @ */;\n}\n\nexport function findLineEnding(src: string, pos: number, len: number): number {\n for (let i = pos; i < len; i++) {\n const ch = src[i];\n if (ch === \"\\n\" || ch === \"\\r\") return i;\n }\n return -1;\n}\n\nexport function skipLineEnding(src: string, pos: PosRef, len: number): void {\n if (pos.value >= len) return;\n\n const byte = src[pos.value];\n if (byte === \"\\n\") {\n pos.value++;\n } else if (byte === \"\\r\") {\n pos.value++;\n if (pos.value < len && src[pos.value] === \"\\n\") {\n pos.value++;\n }\n }\n}\n\nexport function skipQuotedString(src: string, pos: PosRef, len: number, quote: string): void {\n while (pos.value < len) {\n const quotePos = src.indexOf(quote, pos.value);\n\n if (quotePos === -1) {\n pos.value = len;\n return;\n }\n\n pos.value = quotePos;\n\n // Count preceding backslashes\n let backslashCount = 0;\n let checkPos = pos.value - 1;\n while (checkPos >= 0 && src[checkPos] === \"\\\\\") {\n backslashCount++;\n checkPos--;\n }\n\n pos.value++; // Move past the quote\n\n if (backslashCount % 2 === 0) {\n return;\n }\n }\n\n pos.value = len;\n}\n\nexport function skipBlockComment(src: string, pos: PosRef, len: number): void {\n while (pos.value < len) {\n const starPos = src.indexOf(\"*\", pos.value);\n\n if (starPos === -1) {\n pos.value = len;\n return;\n }\n\n pos.value = starPos + 1; // Move past *\n\n if (pos.value < len && src[pos.value] === \"/\") {\n pos.value++; // Move past /\n return;\n }\n }\n\n pos.value = len;\n}\n\nexport function skipLineComment(src: string, pos: PosRef, len: number): void {\n const lineEndPos = findLineEnding(src, pos.value, len);\n\n if (lineEndPos === -1) {\n pos.value = len;\n } else {\n pos.value = lineEndPos;\n skipLineEnding(src, pos, len);\n }\n}\n\nexport function skipLineCommentStoppingAt(\n src: string,\n pos: PosRef,\n len: number,\n stopSequence: string,\n): boolean {\n const seqLen = stopSequence.length;\n\n while (pos.value < len) {\n const byte = src[pos.value];\n\n if (byte === \"\\n\" || byte === \"\\r\") {\n pos.value++;\n return false;\n }\n\n if (pos.value + seqLen <= len && src.slice(pos.value, pos.value + seqLen) === stopSequence) {\n return true;\n }\n\n pos.value++;\n }\n\n return false;\n}\n\nexport function skipLineCommentDetecting(\n src: string,\n pos: PosRef,\n len: number,\n sequences: string[],\n): Array<{ sequence: string; offset: number }> {\n const detected: Array<{ sequence: string; offset: number }> = [];\n\n const lineEndPos = findLineEnding(src, pos.value, len);\n const endPos = lineEndPos === -1 ? len : lineEndPos;\n\n for (const sequence of sequences) {\n const seqLen = sequence.length;\n let searchPos = pos.value;\n\n while (searchPos + seqLen <= endPos) {\n const foundPos = src.indexOf(sequence, searchPos);\n\n if (foundPos === -1 || foundPos >= endPos) {\n break;\n }\n\n detected.push({ sequence, offset: foundPos });\n searchPos = foundPos + seqLen;\n }\n }\n\n if (lineEndPos === -1) {\n pos.value = len;\n } else {\n pos.value = lineEndPos;\n skipLineEnding(src, pos, len);\n }\n\n return detected;\n}\n\nexport function skipHeredoc(src: string, pos: PosRef, len: number): void {\n if (pos.value >= len) return;\n\n const isNowdoc = src[pos.value] === \"'\";\n if (isNowdoc) {\n pos.value++; // Skip opening quote\n }\n\n const delimStart = pos.value;\n while (pos.value < len) {\n const ch = src.charCodeAt(pos.value);\n // alphanumeric or _\n if ((ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57) || ch === 95) {\n pos.value++;\n } else {\n break;\n }\n }\n\n if (pos.value === delimStart) {\n pos.value = len;\n return;\n }\n\n const delimiter = src.slice(delimStart, pos.value);\n\n if (isNowdoc && pos.value < len && src[pos.value] === \"'\") {\n pos.value++;\n }\n\n // Skip to end of opening line\n const lineEndPos = findLineEnding(src, pos.value, len);\n if (lineEndPos === -1) {\n pos.value = len;\n return;\n }\n pos.value = lineEndPos;\n skipLineEnding(src, pos, len);\n\n const delimLen = delimiter.length;\n\n // Scan for closing delimiter on its own line\n while (pos.value < len) {\n if (pos.value + delimLen <= len) {\n const potentialDelim = src.slice(pos.value, pos.value + delimLen);\n\n if (potentialDelim === delimiter) {\n const afterPos = pos.value + delimLen;\n if (\n afterPos >= len ||\n src[afterPos] === \"\\n\" ||\n src[afterPos] === \"\\r\" ||\n src[afterPos] === \";\"\n ) {\n pos.value = afterPos;\n\n if (pos.value < len && src[pos.value] === \";\") {\n pos.value++;\n }\n\n skipLineEnding(src, pos, len);\n return;\n }\n }\n }\n\n // Not the delimiter, skip to next line\n const nextLineEnd = findLineEnding(src, pos.value, len);\n if (nextLineEnd === -1) {\n pos.value = len;\n return;\n }\n pos.value = nextLineEnd;\n skipLineEnding(src, pos, len);\n }\n\n pos.value = len;\n}\n\nexport function skipTemplateLiteral(src: string, pos: PosRef, len: number): void {\n while (pos.value < len) {\n const byte = src[pos.value];\n\n if (byte === \"`\") {\n pos.value++;\n return;\n } else if (byte === \"\\\\\") {\n pos.value += 2;\n } else if (byte === \"$\" && pos.value + 1 < len && src[pos.value + 1] === \"{\") {\n // Template expression ${...}\n pos.value += 2;\n skipTemplateExpression(src, pos, len);\n } else {\n pos.value++;\n }\n }\n}\n\nexport function skipTemplateExpression(src: string, pos: PosRef, len: number): void {\n let depth = 1;\n\n while (depth > 0 && pos.value < len) {\n const byte = src[pos.value];\n\n if (byte === \"{\") {\n depth++;\n pos.value++;\n } else if (byte === \"}\") {\n depth--;\n pos.value++;\n } else if (byte === \"'\" || byte === '\"') {\n pos.value++;\n skipQuotedString(src, pos, len, byte);\n } else if (byte === \"`\") {\n pos.value++;\n skipTemplateLiteral(src, pos, len);\n } else if (byte === \"/\" && pos.value + 1 < len) {\n const next = src[pos.value + 1];\n if (next === \"/\") {\n pos.value += 2;\n skipLineComment(src, pos, len);\n } else if (next === \"*\") {\n pos.value += 2;\n skipBlockComment(src, pos, len);\n } else {\n pos.value++;\n }\n } else {\n pos.value++;\n }\n }\n}\n\nexport function skipBacktickString(src: string, pos: PosRef, len: number): void {\n skipQuotedString(src, pos, len, \"`\");\n}\n","import {\r\n TokenType,\r\n State,\r\n type IgnoreRangeRegion,\r\n type IgnoreRangeResumeState,\r\n type Token,\r\n} from \"./types.js\";\r\nimport { Directives } from \"./directives.js\";\r\nimport { ErrorReason, type LexerError } from \"./errors.js\";\r\nimport { isFrontendEventStyleAtName } from \"../frontend-attribute-names.js\";\r\nimport {\r\n canStartBladeDirectiveAt,\r\n isAsciiAlnum as isAlnum,\r\n isAsciiAlpha as isAlpha,\r\n skipQuotedString,\r\n skipBacktickString,\r\n skipBlockComment,\r\n skipLineCommentDetecting,\r\n skipHeredoc,\r\n skipTemplateLiteral,\r\n type PosRef,\r\n} from \"./scan-primitives.js\";\r\n\r\nconst RAWTEXT_ELEMENTS = new Set([\"script\", \"style\"]);\r\nconst ATTR_LIKE_AT_NAME_CONTINUATION = new Set([\".\", \"-\", \":\", \"[\", \"]\"]);\r\n\r\nexport interface LexerResult {\r\n tokens: Token[];\r\n errors: LexerError[];\r\n}\r\n\r\nexport interface LexerRawBlockConfig {\r\n verbatimStartDirectives?: readonly string[];\r\n verbatimEndDirectives?: readonly string[];\r\n ignoreRanges?: readonly IgnoreRangeRegion[];\r\n ignoreRangeCollector?: IgnoreRangeCollector | null;\r\n}\r\n\r\ninterface IgnoreRangeCollector {\r\n handleBladeComment(\r\n start: number,\r\n end: number,\r\n originState: State,\r\n tagStart: number | null,\r\n resume: IgnoreRangeResumeState,\r\n ): void;\r\n handleHtmlComment(\r\n start: number,\r\n end: number,\r\n originState: State,\r\n tagStart: number | null,\r\n resume: IgnoreRangeResumeState,\r\n ): void;\r\n finish(resume: IgnoreRangeResumeState, eof: number): IgnoreRangeRegion[];\r\n}\r\n\r\nexport class Lexer {\r\n private src: string;\r\n private pos = 0;\r\n private len: number;\r\n private state: State = State.Data;\r\n private returnState: State = State.Data;\r\n private tokens: Token[] = [];\r\n private errors: LexerError[] = [];\r\n private verbatim = false;\r\n private verbatimReturnState: State | null = null;\r\n private verbatimStartTokenIndex: number | null = null;\r\n private phpBlock = false;\r\n private phpBlockStartTokenIndex: number | null = null;\r\n private phpTag = false;\r\n private attrPhpDirectiveDepth = 0;\r\n private rawtextTagName = \"\";\r\n private currentTagName = \"\";\r\n private currentTagStart = -1;\r\n private ignoreRanges: readonly IgnoreRangeRegion[];\r\n private nextIgnoreRangeIndex = 0;\r\n private ignoreRangeCollector: IgnoreRangeCollector | null;\r\n private pendingHtmlCommentOriginState: State | null = null;\r\n private pendingHtmlCommentTagStart: number | null = null;\r\n private pendingBladeCommentOriginState: State | null = null;\r\n private pendingBladeCommentTagStart: number | null = null;\r\n\r\n private isAtAttributeCandidate(nameLower: string, afterNamePos: number): boolean {\r\n if (afterNamePos >= this.len) {\r\n return isFrontendEventStyleAtName(nameLower);\r\n }\r\n\r\n const immediate = this.src[afterNamePos];\r\n if (ATTR_LIKE_AT_NAME_CONTINUATION.has(immediate)) {\r\n return true;\r\n }\r\n\r\n let pos = afterNamePos;\r\n while (pos < this.len && isSpace(this.src.charCodeAt(pos))) {\r\n pos++;\r\n }\r\n\r\n if (pos < this.len && this.src[pos] === \"=\") {\r\n return true;\r\n }\r\n\r\n if (!isFrontendEventStyleAtName(nameLower)) {\r\n return false;\r\n }\r\n\r\n return (\r\n pos >= this.len ||\r\n this.src[pos] === \">\" ||\r\n this.src[pos] === \"/\" ||\r\n isSpace(immediate.charCodeAt(0))\r\n );\r\n }\r\n private isClosingTag = false;\r\n private continuedTagName = false;\r\n private inXmlDeclaration = false;\r\n private _directives: Directives;\r\n private verbatimStartDirectives = new Set<string>([\"verbatim\"]);\r\n private verbatimEndDirectives = new Set<string>([\"endverbatim\"]);\r\n\r\n constructor(source: string, directives?: Directives, rawBlockConfig?: LexerRawBlockConfig) {\r\n this.src = source;\r\n this.len = source.length;\r\n this._directives = directives ?? Directives.acceptAll();\r\n this.ignoreRanges = rawBlockConfig?.ignoreRanges ?? [];\r\n this.ignoreRangeCollector = rawBlockConfig?.ignoreRangeCollector ?? null;\r\n\r\n for (const directive of rawBlockConfig?.verbatimStartDirectives ?? []) {\r\n const normalized = normalizeDirectiveName(directive);\r\n if (normalized) this.verbatimStartDirectives.add(normalized);\r\n }\r\n\r\n for (const directive of rawBlockConfig?.verbatimEndDirectives ?? []) {\r\n const normalized = normalizeDirectiveName(directive);\r\n if (normalized) this.verbatimEndDirectives.add(normalized);\r\n }\r\n }\r\n\r\n directives(): Directives {\r\n return this._directives;\r\n }\r\n\r\n tokenize(): LexerResult {\r\n while (this.pos < this.len) {\r\n if (this.tryEmitIgnoreRange()) {\r\n continue;\r\n }\r\n\r\n switch (this.state) {\r\n case State.Data:\r\n this.scanData();\r\n break;\r\n case State.RawText:\r\n this.scanRawtext();\r\n break;\r\n case State.BladeComment:\r\n this.scanBladeCommentContent();\r\n break;\r\n case State.Comment:\r\n this.scanComment();\r\n break;\r\n case State.TagName:\r\n this.scanTagName();\r\n break;\r\n case State.BeforeAttrName:\r\n this.scanBeforeAttrName();\r\n break;\r\n case State.AttrName:\r\n this.scanAttrName();\r\n break;\r\n case State.AfterAttrName:\r\n this.scanAfterAttrName();\r\n break;\r\n case State.BeforeAttrValue:\r\n this.scanBeforeAttrValue();\r\n break;\r\n case State.AttrValueQuoted:\r\n this.scanAttrValueQuoted();\r\n break;\r\n case State.AttrValueUnquoted:\r\n this.scanAttrValueUnquoted();\r\n break;\r\n default:\r\n break;\r\n }\r\n }\r\n\r\n // EOF in tag - emit SyntheticClose\r\n if (\r\n this.state === State.TagName ||\r\n this.state === State.BeforeAttrName ||\r\n this.state === State.AttrName ||\r\n this.state === State.AfterAttrName ||\r\n this.state === State.BeforeAttrValue ||\r\n this.state === State.AttrValueQuoted ||\r\n this.state === State.AttrValueUnquoted\r\n ) {\r\n this.emit(TokenType.SyntheticClose, this.pos, this.pos);\r\n }\r\n\r\n this.ignoreRangeCollector?.finish(this.snapshotIgnoreRangeResumeState(), this.len);\r\n\r\n return { tokens: this.tokens, errors: this.errors };\r\n }\r\n\r\n private emit(type: TokenType, start: number, end: number): void {\r\n this.tokens.push({ type, start, end });\r\n }\r\n\r\n private snapshotIgnoreRangeResumeState(): IgnoreRangeResumeState {\r\n return {\r\n state: this.state,\r\n returnState: this.returnState,\r\n rawtextTagName: this.rawtextTagName,\r\n currentTagName: this.currentTagName,\r\n isClosingTag: this.isClosingTag,\r\n continuedTagName: this.continuedTagName,\r\n inXmlDeclaration: this.inXmlDeclaration,\r\n verbatim: this.verbatim,\r\n verbatimReturnState: this.verbatimReturnState,\r\n verbatimStartTokenIndex: this.verbatimStartTokenIndex,\r\n phpBlock: this.phpBlock,\r\n phpBlockStartTokenIndex: this.phpBlockStartTokenIndex,\r\n phpTag: this.phpTag,\r\n attrPhpDirectiveDepth: this.attrPhpDirectiveDepth,\r\n };\r\n }\r\n\r\n private restoreIgnoreRangeResumeState(resume: IgnoreRangeResumeState): void {\r\n this.state = resume.state;\r\n this.returnState = resume.returnState;\r\n this.rawtextTagName = resume.rawtextTagName;\r\n this.currentTagName = resume.currentTagName;\r\n this.isClosingTag = resume.isClosingTag;\r\n this.continuedTagName = resume.continuedTagName;\r\n this.inXmlDeclaration = resume.inXmlDeclaration;\r\n this.verbatim = resume.verbatim;\r\n this.verbatimReturnState = resume.verbatimReturnState;\r\n this.verbatimStartTokenIndex = resume.verbatimStartTokenIndex ?? null;\r\n this.phpBlock = resume.phpBlock;\r\n this.phpBlockStartTokenIndex = resume.phpBlockStartTokenIndex ?? null;\r\n this.phpTag = resume.phpTag;\r\n this.attrPhpDirectiveDepth = resume.attrPhpDirectiveDepth;\r\n }\r\n\r\n private tryEmitIgnoreRange(): boolean {\r\n const range = this.ignoreRanges[this.nextIgnoreRangeIndex];\r\n if (!range || this.pos !== range.start) {\r\n return false;\r\n }\r\n\r\n this.emit(TokenType.IgnoreRange, range.start, range.end);\r\n this.pos = range.end;\r\n this.restoreIgnoreRangeResumeState(range.resume);\r\n this.nextIgnoreRangeIndex++;\r\n return true;\r\n }\r\n\r\n private nextIgnoreRangeStart(): number | null {\r\n const range = this.ignoreRanges[this.nextIgnoreRangeIndex];\r\n return range ? range.start : null;\r\n }\r\n\r\n private recordBladeComment(\r\n start: number,\r\n end: number,\r\n originState: State,\r\n tagStart: number | null,\r\n ): void {\r\n this.ignoreRangeCollector?.handleBladeComment(\r\n start,\r\n end,\r\n originState,\r\n tagStart,\r\n this.snapshotIgnoreRangeResumeState(),\r\n );\r\n }\r\n\r\n private recordHtmlComment(\r\n start: number,\r\n end: number,\r\n originState: State,\r\n tagStart: number | null,\r\n ): void {\r\n this.ignoreRangeCollector?.handleHtmlComment(\r\n start,\r\n end,\r\n originState,\r\n tagStart,\r\n this.snapshotIgnoreRangeResumeState(),\r\n );\r\n }\r\n\r\n private beginBladeCommentCapture(originState: State, tagStart: number | null): void {\r\n this.pendingBladeCommentOriginState = originState;\r\n this.pendingBladeCommentTagStart = tagStart;\r\n }\r\n\r\n private beginHtmlCommentCapture(originState: State, tagStart: number | null): void {\r\n this.pendingHtmlCommentOriginState = originState;\r\n this.pendingHtmlCommentTagStart = tagStart;\r\n }\r\n\r\n private logError(reason: ErrorReason, offset: number): void {\r\n this.errors.push({ reason, offset });\r\n }\r\n\r\n private peek(): string | null {\r\n return this.pos < this.len ? this.src[this.pos] : null;\r\n }\r\n\r\n private peekAhead(n: number): string | null {\r\n const p = this.pos + n;\r\n return p < this.len ? this.src[p] : null;\r\n }\r\n\r\n private skipWhitespace(): void {\r\n while (this.pos < this.len && isSpace(this.src.charCodeAt(this.pos))) {\r\n this.pos++;\r\n }\r\n }\r\n\r\n private skipAndEmitWhitespace(): void {\r\n const start = this.pos;\r\n while (this.pos < this.len && isSpace(this.src.charCodeAt(this.pos))) {\r\n this.pos++;\r\n }\r\n if (start < this.pos) {\r\n this.emit(TokenType.Whitespace, start, this.pos);\r\n }\r\n }\r\n\r\n private scanAttributePhpDirectiveContent(): boolean {\r\n if (this.attrPhpDirectiveDepth <= 0) return false;\r\n\r\n const start = this.pos;\r\n\r\n while (this.pos < this.len) {\r\n const ch = this.src[this.pos];\r\n\r\n if (ch === '\"' || ch === \"'\") {\r\n this.pos++;\r\n this.skipQuotedStringPrim(ch);\r\n continue;\r\n }\r\n\r\n if (ch === \"`\") {\r\n this.pos++;\r\n this.skipBacktickStringPrim();\r\n continue;\r\n }\r\n\r\n if (ch === \"/\" && this.pos + 1 < this.len) {\r\n const next = this.src[this.pos + 1];\r\n if (next === \"/\") {\r\n this.pos += 2;\r\n while (this.pos < this.len) {\r\n const c = this.src[this.pos];\r\n if (c === \"\\n\" || c === \"\\r\") {\r\n this.pos++;\r\n break;\r\n }\r\n this.pos++;\r\n }\r\n continue;\r\n }\r\n if (next === \"*\") {\r\n this.pos += 2;\r\n this.skipBlockCommentPrim();\r\n continue;\r\n }\r\n }\r\n\r\n if (ch === \"#\") {\r\n this.pos++;\r\n while (this.pos < this.len) {\r\n const c = this.src[this.pos];\r\n if (c === \"\\n\" || c === \"\\r\") {\r\n this.pos++;\r\n break;\r\n }\r\n this.pos++;\r\n }\r\n continue;\r\n }\r\n\r\n if (\r\n ch === \"<\" &&\r\n this.pos + 2 < this.len &&\r\n this.src[this.pos + 1] === \"<\" &&\r\n this.src[this.pos + 2] === \"<\"\r\n ) {\r\n this.pos += 3;\r\n this.skipHeredocPrim();\r\n continue;\r\n }\r\n\r\n if (ch === \"@\" && !this.verbatim && !this.phpTag) {\r\n let nameEnd = this.pos + 1;\r\n while (nameEnd < this.len) {\r\n const cc = this.src.charCodeAt(nameEnd);\r\n if (isAlnum(cc) || cc === 95) {\r\n nameEnd++;\r\n } else {\r\n break;\r\n }\r\n }\r\n\r\n if (nameEnd > this.pos + 1) {\r\n const name = this.src.slice(this.pos + 1, nameEnd).toLowerCase();\r\n if (name === \"php\" || name === \"endphp\") {\r\n if (start < this.pos) {\r\n this.emit(TokenType.Text, start, this.pos);\r\n }\r\n this.returnState = this.state;\r\n this.scanDirective();\r\n return true;\r\n }\r\n }\r\n }\r\n\r\n this.pos++;\r\n }\r\n\r\n if (start < this.pos) {\r\n this.emit(TokenType.Text, start, this.pos);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n private posRef(): PosRef {\r\n return { value: this.pos };\r\n }\r\n\r\n private syncPos(ref: PosRef): void {\r\n this.pos = ref.value;\r\n }\r\n\r\n private skipQuotedStringPrim(quote: string): void {\r\n const ref = this.posRef();\r\n skipQuotedString(this.src, ref, this.len, quote);\r\n this.syncPos(ref);\r\n }\r\n\r\n private skipBlockCommentPrim(): void {\r\n const ref = this.posRef();\r\n skipBlockComment(this.src, ref, this.len);\r\n this.syncPos(ref);\r\n }\r\n\r\n private skipHeredocPrim(): void {\r\n const ref = this.posRef();\r\n skipHeredoc(this.src, ref, this.len);\r\n this.syncPos(ref);\r\n }\r\n\r\n private skipBacktickStringPrim(): void {\r\n const ref = this.posRef();\r\n skipBacktickString(this.src, ref, this.len);\r\n this.syncPos(ref);\r\n }\r\n\r\n private skipTemplateLiteralPrim(): void {\r\n const ref = this.posRef();\r\n skipTemplateLiteral(this.src, ref, this.len);\r\n this.syncPos(ref);\r\n }\r\n\r\n /**\r\n * Skip line comment with warnings about ?> inside comments.\r\n */\r\n private skipLineCommentWithWarnings(): void {\r\n const ref = this.posRef();\r\n const detected = skipLineCommentDetecting(this.src, ref, this.len, [\"?>\"]);\r\n this.syncPos(ref);\r\n\r\n for (const d of detected) {\r\n this.logError(ErrorReason.PhpCloseTagInComment, d.offset);\r\n }\r\n }\r\n\r\n /**\r\n * Detect if the current position starts a Blade/PHP construct.\r\n * Used for construct collision detection inside echos.\r\n */\r\n private detectConstruct(): boolean {\r\n if (this.pos >= this.len) return false;\r\n const byte = this.src[this.pos];\r\n\r\n // Triple echo: {{{\r\n if (byte === \"{\" && this.peekAhead(1) === \"{\" && this.peekAhead(2) === \"{\") {\r\n return true;\r\n }\r\n\r\n // Blade comment: {{--\r\n if (\r\n byte === \"{\" &&\r\n this.peekAhead(1) === \"{\" &&\r\n this.peekAhead(2) === \"-\" &&\r\n this.peekAhead(3) === \"-\"\r\n ) {\r\n return true;\r\n }\r\n\r\n // Raw echo: {!!\r\n if (byte === \"{\" && this.peekAhead(1) === \"!\" && this.peekAhead(2) === \"!\") {\r\n return true;\r\n }\r\n\r\n // Echo: {{\r\n if (byte === \"{\" && this.peekAhead(1) === \"{\") {\r\n return true;\r\n }\r\n\r\n // Directive: @word\r\n if (byte === \"@\" && this.pos + 1 < this.len) {\r\n const next = this.src[this.pos + 1];\r\n if (isAlpha(next.charCodeAt(0))) {\r\n return true;\r\n }\r\n }\r\n\r\n // PHP tag: <?php, <?=\r\n if (byte === \"<\" && this.peekAhead(1) === \"?\") {\r\n if (this.phpTagStartLength(this.pos) > 0) {\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n }\r\n\r\n /**\r\n * Case-insensitive ASCII word match at a source offset.\r\n */\r\n private hasAsciiWordAt(pos: number, word: string): boolean {\r\n if (pos + word.length > this.len) return false;\r\n for (let i = 0; i < word.length; i++) {\r\n let code = this.src.charCodeAt(pos + i);\r\n if (code >= 65 && code <= 90) code += 32;\r\n if (code !== word.charCodeAt(i)) {\r\n return false;\r\n }\r\n }\r\n return true;\r\n }\r\n\r\n /**\r\n * Fast-forward to next character relevant to TSX generic scanning.\r\n */\r\n private nextInterestingPosForTsxGeneric(fromPos: number): number {\r\n for (let i = fromPos; i < this.len; i++) {\r\n switch (this.src.charCodeAt(i)) {\r\n case 60: // <\r\n case 62: // >\r\n case 34: // \"\r\n case 39: // '\r\n case 96: // `\r\n case 64: // @\r\n case 10: // \\n\r\n case 13: // \\r\r\n return i;\r\n }\r\n }\r\n return this.len;\r\n }\r\n\r\n /**\r\n * Fast-forward to next character relevant to balanced JS-like scanning.\r\n */\r\n private nextInterestingPosForBalancedJsLike(fromPos: number): number {\r\n for (let i = fromPos; i < this.len; i++) {\r\n switch (this.src.charCodeAt(i)) {\r\n case 123: // {\r\n case 125: // }\r\n case 40: // (\r\n case 41: // )\r\n case 34: // \"\r\n case 39: // '\r\n case 96: // `\r\n case 47: // /\r\n case 64: // @\r\n case 10: // \\n\r\n case 13: // \\r\r\n return i;\r\n }\r\n }\r\n return this.len;\r\n }\r\n\r\n private scanData(): void {\r\n const start = this.pos;\r\n\r\n while (this.pos < this.len) {\r\n const nextIgnoreRangeStart = this.nextIgnoreRangeStart();\r\n if (nextIgnoreRangeStart !== null && this.pos === nextIgnoreRangeStart) {\r\n if (start < this.pos) {\r\n if (this.phpBlock) {\r\n this.emit(TokenType.PhpBlock, start, this.pos);\r\n } else if (this.phpTag) {\r\n this.emit(TokenType.PhpContent, start, this.pos);\r\n } else {\r\n this.emit(TokenType.Text, start, this.pos);\r\n }\r\n }\r\n return;\r\n }\r\n\r\n const byte = this.src[this.pos];\r\n\r\n if (this.phpBlock) {\r\n if (byte === \"'\" || byte === '\"') {\r\n this.pos++;\r\n this.skipQuotedStringPrim(byte);\r\n continue;\r\n }\r\n\r\n if (byte === \"`\") {\r\n this.pos++;\r\n this.skipBacktickStringPrim();\r\n continue;\r\n }\r\n\r\n if (byte === \"/\" && this.pos + 1 < this.len) {\r\n const next = this.src[this.pos + 1];\r\n if (next === \"/\") {\r\n this.pos += 2;\r\n while (this.pos < this.len) {\r\n const ch = this.src[this.pos];\r\n if (ch === \"\\n\" || ch === \"\\r\") {\r\n this.pos++;\r\n break;\r\n }\r\n this.pos++;\r\n }\r\n continue;\r\n }\r\n if (next === \"*\") {\r\n this.pos += 2;\r\n this.skipBlockCommentPrim();\r\n continue;\r\n }\r\n }\r\n\r\n if (byte === \"#\") {\r\n this.pos++;\r\n while (this.pos < this.len) {\r\n const ch = this.src[this.pos];\r\n if (ch === \"\\n\" || ch === \"\\r\") {\r\n this.pos++;\r\n break;\r\n }\r\n this.pos++;\r\n }\r\n continue;\r\n }\r\n\r\n if (\r\n byte === \"<\" &&\r\n this.pos + 2 < this.len &&\r\n this.src[this.pos + 1] === \"<\" &&\r\n this.src[this.pos + 2] === \"<\"\r\n ) {\r\n this.pos += 3;\r\n this.skipHeredocPrim();\r\n continue;\r\n }\r\n\r\n if (byte === \"@\" && this.isEndphpAt(this.pos)) {\r\n if (start < this.pos) {\r\n this.emit(TokenType.PhpBlock, start, this.pos);\r\n }\r\n this.scanDirective();\r\n return;\r\n }\r\n\r\n this.pos++;\r\n continue;\r\n }\r\n\r\n if (byte === \"{\" && !this.verbatim && !this.phpBlock && !this.phpTag) {\r\n // Escaped echo: previous char is @\r\n const prevChar = this.pos > 0 ? this.src[this.pos - 1] : null;\r\n if (prevChar === \"@\") {\r\n this.pos++;\r\n continue;\r\n }\r\n\r\n const next1 = this.peekAhead(1);\r\n\r\n if (next1 === \"{\") {\r\n const next2 = this.peekAhead(2);\r\n const next3 = this.peekAhead(3);\r\n if (start < this.pos) {\r\n this.emit(TokenType.Text, start, this.pos);\r\n }\r\n\r\n if (next2 === \"-\" && next3 === \"-\") {\r\n this.beginBladeCommentCapture(State.Data, null);\r\n this.scanBladeCommentStart();\r\n return;\r\n }\r\n this.scanEcho();\r\n return;\r\n } else if (next1 === \"!\" && this.pos + 2 < this.len && this.src[this.pos + 2] === \"!\") {\r\n if (start < this.pos) {\r\n this.emit(TokenType.Text, start, this.pos);\r\n }\r\n this.scanRawEcho();\r\n return;\r\n } else {\r\n this.pos++;\r\n }\r\n } else if (\r\n this.verbatim &&\r\n this.verbatimReturnState === State.RawText &&\r\n this.rawtextTagName.length > 0 &&\r\n byte === \"<\" &&\r\n this.isRawtextClosingTagAt(this.pos, this.rawtextTag