From 76a1e6afe87d7b089a19303bbc325e99cae2e30f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 3 Sep 2020 09:21:14 +0200 Subject: [PATCH] (refactor) move htmlx2jsx handlers into own files --- packages/svelte2tsx/src/htmlxtojsx/index.ts | 643 ++---------------- .../src/htmlxtojsx/nodes/action-directive.ts | 29 + .../htmlxtojsx/nodes/animation-directive.ts | 27 + .../src/htmlxtojsx/nodes/attribute.ts | 167 +++++ .../nodes/{await-block.ts => await.ts} | 0 .../src/htmlxtojsx/nodes/binding.ts | 82 +++ .../src/htmlxtojsx/nodes/class-directive.ts | 15 + .../src/htmlxtojsx/nodes/comment.ts | 9 + .../src/htmlxtojsx/nodes/component-type.ts | 9 - .../src/htmlxtojsx/nodes/component.ts | 85 +++ .../svelte2tsx/src/htmlxtojsx/nodes/debug.ts | 10 + .../svelte2tsx/src/htmlxtojsx/nodes/each.ts | 42 ++ .../src/htmlxtojsx/nodes/element.ts | 22 + .../src/htmlxtojsx/nodes/event-handler.ts | 50 ++ .../src/htmlxtojsx/nodes/if-else.ts | 39 ++ .../src/htmlxtojsx/nodes/raw-html.ts | 10 + .../src/htmlxtojsx/nodes/svelte-tag.ts | 17 + .../htmlxtojsx/nodes/transition-directive.ts | 33 + .../src/htmlxtojsx/utils/node-utils.ts | 28 + packages/svelte2tsx/src/nodes/slot.ts | 2 +- 20 files changed, 721 insertions(+), 598 deletions(-) create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts rename packages/svelte2tsx/src/htmlxtojsx/nodes/{await-block.ts => await.ts} (100%) create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts delete mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/component-type.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts create mode 100644 packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts index 2fa344bcd..66a06315b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts @@ -1,655 +1,117 @@ +import { Node } from 'estree-walker'; import MagicString from 'magic-string'; import svelte from 'svelte/compiler'; -import { Node } from 'estree-walker'; import { parseHtmlx } from '../htmlxparser'; -import svgAttributes from './svgattributes'; -import { getTypeForComponent } from './nodes/component-type'; -import { handleAwait } from './nodes/await-block'; -import { getSlotName } from '../utils/svelteAst'; -import { getSingleSlotDef } from '../nodes/slot'; - -type ElementType = string; -const oneWayBindingAttributes: Map = new Map( - ['clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight'] - .map((e) => [e, 'HTMLDivElement'] as [string, string]) - .concat( - ['duration', 'buffered', 'seekable', 'seeking', 'played', 'ended'].map((e) => [ - e, - 'HTMLMediaElement', - ]), - ), -); - -/** - * List taken from `svelte-jsx.d.ts` by searching for all attributes of type number - */ -const numberOnlyAttributes = new Set([ - 'cols', - 'colspan', - 'currenttime', - 'defaultplaybackrate', - 'high', - 'low', - 'marginheight', - 'marginwidth', - 'minlength', - 'maxlength', - 'optimum', - 'rows', - 'rowspan', - 'size', - 'span', - 'start', - 'tabindex', - 'results', - 'volume', -]); - -const beforeStart = (start: number) => start - 1; +import { handleActionDirective } from './nodes/action-directive'; +import { handleAnimateDirective } from './nodes/animation-directive'; +import { handleAttribute } from './nodes/attribute'; +import { handleAwait } from './nodes/await'; +import { handleBinding } from './nodes/binding'; +import { handleClassDirective } from './nodes/class-directive'; +import { handleComment } from './nodes/comment'; +import { handleComponent } from './nodes/component'; +import { handleDebug } from './nodes/debug'; +import { handleEach } from './nodes/each'; +import { handleElement } from './nodes/element'; +import { handleEventHandler } from './nodes/event-handler'; +import { handleElse, handleIf } from './nodes/if-else'; +import { handleRawHtml } from './nodes/raw-html'; +import { handleSvelteTag } from './nodes/svelte-tag'; +import { handleTransitionDirective } from './nodes/transition-directive'; type Walker = (node: Node, parent: Node, prop: string, index: number) => void; -const stripDoctype = (str: MagicString) => { +function stripDoctype(str: MagicString): void { const regex = /(\n)?/i; const result = regex.exec(str.original); - if (result) str.remove(result.index, result.index + result[0].length); -}; + if (result) { + str.remove(result.index, result.index + result[0].length); + } +} +/** + * Walks the HTMLx part of the Svelte component + * and converts it to JSX + */ export function convertHtmlxToJsx( str: MagicString, ast: Node, onWalk: Walker = null, onLeave: Walker = null, -) { +): void { const htmlx = str.original; stripDoctype(str); str.prepend('<>'); str.append(''); - const handleRaw = (rawBlock: Node) => { - const tokenStart = htmlx.indexOf('@html', rawBlock.start); - str.remove(tokenStart, tokenStart + '@html'.length); - }; - const handleDebug = (debugBlock: Node) => { - const tokenStart = htmlx.indexOf('@debug', debugBlock.start); - str.remove(tokenStart, tokenStart + '@debug'.length); - }; - - const handleEventHandler = (attr: Node, parent: Node) => { - const jsxEventName = attr.name; - - if ( - ['Element', 'Window', 'Body'].includes( - parent.type, - ) /*&& KnownEvents.indexOf('on'+jsxEventName) >= 0*/ - ) { - if (attr.expression) { - const endAttr = htmlx.indexOf('=', attr.start); - str.overwrite(attr.start + 'on:'.length - 1, endAttr, jsxEventName); - if (htmlx[attr.end - 1] == '"') { - const firstQuote = htmlx.indexOf('"', endAttr); - str.remove(firstQuote, firstQuote + 1); - str.remove(attr.end - 1, attr.end); - } - } else { - str.overwrite( - attr.start + 'on:'.length - 1, - attr.end, - `${jsxEventName}={undefined}`, - ); - } - } else { - if (attr.expression) { - const on = 'on'; - //for handler assignment, we changeIt to call to our __sveltets_ensureFunction - str.appendRight( - attr.start, - `{__sveltets_instanceOf(${getTypeForComponent(parent)}).$`, - ); - const eventNameIndex = htmlx.indexOf(':', attr.start) + 1; - str.overwrite(htmlx.indexOf(on, attr.start) + on.length, eventNameIndex, `('`); - const eventEnd = htmlx.lastIndexOf('=', attr.expression.start); - str.overwrite(eventEnd, attr.expression.start, `', `); - str.overwrite(attr.expression.end, attr.end, ')}'); - str.move(attr.start, attr.end, parent.end); - } else { - //for passthrough handlers, we just remove - str.remove(attr.start, attr.end); - } - } - }; - - const handleClassDirective = (attr: Node) => { - str.overwrite(attr.start, attr.expression.start, `{...__sveltets_ensureType(Boolean, !!(`); - const endBrackets = `))}`; - if (attr.end !== attr.expression.end) { - str.overwrite(attr.expression.end, attr.end, endBrackets); - } else { - str.appendLeft(attr.end, endBrackets); - } - }; - - const handleActionDirective = (attr: Node, parent: Node) => { - str.overwrite( - attr.start, - attr.start + 'use:'.length, - `{...__sveltets_ensureAction(__sveltets_mapElementTag('${parent.name}'),`, - ); - - if (!attr.expression) { - str.appendLeft(attr.end, ')}'); - return; - } - - str.overwrite(attr.start + `use:${attr.name}`.length, attr.expression.start, ','); - str.appendLeft(attr.expression.end, ')'); - if (htmlx[attr.end - 1] == '"') { - str.remove(attr.end - 1, attr.end); - } - }; - - const handleTransitionDirective = (attr: Node) => { - str.overwrite( - attr.start, - htmlx.indexOf(':', attr.start) + 1, - '{...__sveltets_ensureTransition(', - ); - - if (attr.modifiers.length) { - const local = htmlx.indexOf('|', attr.start); - str.remove(local, attr.expression ? attr.expression.start : attr.end); - } - - if (!attr.expression) { - str.appendLeft(attr.end, ', {})}'); - return; - } - - str.overwrite( - htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, - attr.expression.start, - ', ', - ); - str.appendLeft(attr.expression.end, ')'); - if (htmlx[attr.end - 1] == '"') { - str.remove(attr.end - 1, attr.end); - } - }; - - const handleAnimateDirective = (attr: Node) => { - str.overwrite( - attr.start, - htmlx.indexOf(':', attr.start) + 1, - '{...__sveltets_ensureAnimation(', - ); - - if (!attr.expression) { - str.appendLeft(attr.end, ', {})}'); - return; - } - str.overwrite( - htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, - attr.expression.start, - ', ', - ); - str.appendLeft(attr.expression.end, ')'); - if (htmlx[attr.end - 1] == '"') { - str.remove(attr.end - 1, attr.end); - } - }; - - const isShortHandAttribute = (attr: Node) => { - return attr.expression.end === attr.end; - }; - - const handleBinding = (attr: Node, el: Node) => { - const getThisType = (node: Node) => { - switch (node.type) { - case 'InlineComponent': - return getTypeForComponent(node); - case 'Element': - return `__sveltets_ctorOf(__sveltets_mapElementTag('${node.name}'))`; - case 'Body': - return 'HTMLBodyElement'; - default: - break; - } - }; - - //bind group on input - if (attr.name == 'group' && el.name == 'input') { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, '{...__sveltets_empty('); - - const endBrackets = ')}'; - if (isShortHandAttribute(attr)) { - str.prependRight(attr.end, endBrackets); - } else { - str.overwrite(attr.expression.end, attr.end, endBrackets); - } - return; - } - - const supportsBindThis = ['InlineComponent', 'Element', 'Body']; - - //bind this - if (attr.name == 'this' && supportsBindThis.includes(el.type)) { - const thisType = getThisType(el); - - if (thisType) { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, `{...__sveltets_ensureType(${thisType}, `); - str.overwrite(attr.expression.end, attr.end, ')}'); - return; - } - } - - //one way binding - if (oneWayBindingAttributes.has(attr.name) && el.type == 'Element') { - str.remove(attr.start, attr.expression.start); - str.appendLeft(attr.expression.start, `{...__sveltets_empty(`); - if (isShortHandAttribute(attr)) { - // eslint-disable-next-line max-len - str.appendLeft( - attr.end, - `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${ - attr.name - })}`, - ); - } else { - // eslint-disable-next-line max-len - str.overwrite( - attr.expression.end, - attr.end, - `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${ - attr.name - })}`, - ); - } - return; - } - - str.remove(attr.start, attr.start + 'bind:'.length); - if (attr.expression.start == attr.start + 'bind:'.length) { - str.prependLeft(attr.expression.start, `${attr.name}={`); - str.appendLeft(attr.end, `}`); - return; - } - - //remove possible quotes - if (htmlx[attr.end - 1] == '"') { - const firstQuote = htmlx.indexOf('"', attr.start); - str.remove(firstQuote, firstQuote + 1); - str.remove(attr.end - 1, attr.end); - } - }; - - const handleSlot = (slotEl: Node, component: Node, slotName: string) => { - //collect "let" definitions - let hasMoved = false; - let afterTag: number; - for (const attr of slotEl.attributes) { - if (attr.type != 'Let') continue; - - if (slotEl.children.length == 0) { - //no children anyway, just wipe out the attribute - str.remove(attr.start, attr.end); - continue; - } - - afterTag = afterTag || htmlx.lastIndexOf('>', slotEl.children[0].start) + 1; - - str.move(attr.start, attr.end, afterTag); - - //remove let: - if (hasMoved) { - str.overwrite(attr.start, attr.start + 'let:'.length, ', '); - } else { - str.remove(attr.start, attr.start + 'let:'.length); - } - hasMoved = true; - if (attr.expression) { - //overwrite the = as a : - const equalSign = htmlx.lastIndexOf('=', attr.expression.start); - const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); - str.overwrite(equalSign, curly + 1, ':'); - str.remove(attr.expression.end, attr.end); - } - } - if (!hasMoved) return; - str.appendLeft(afterTag, '{() => { let {'); - str.appendRight( - afterTag, - `} = ${getSingleSlotDef(component, slotName)}`+ ';<>', - ); - - const closeTagStart = htmlx.lastIndexOf('<', slotEl.end); - str.appendLeft(closeTagStart, '}}'); - }; - - const handleComponent = (el: Node) => { - //we need to remove : if it is a svelte component - if (el.name.startsWith('svelte:')) { - const colon = htmlx.indexOf(':', el.start); - str.remove(colon, colon + 1); - - const closeTag = htmlx.lastIndexOf('/' + el.name, el.end); - if (closeTag > el.start) { - const colon = htmlx.indexOf(':', closeTag); - str.remove(colon, colon + 1); - } - } - - //we only need to do something if there is a let or slot - handleSlot(el, el, 'default'); - - //walk the direct children looking for slots. We do this here because we need the name of our component for handleSlot - //we could lean on leave/enter, but I am lazy - if (!el.children) return; - for (const child of el.children) { - const slotName = getSlotName(child); - if (slotName) { - handleSlot(child, el, slotName); - } - } - }; - - const handleAttribute = (attr: Node, parent: Node) => { - let transformedFromDirectiveOrNamespace = false; - - //if we are on an "element" we are case insensitive, lowercase to match our JSX - if (parent.type == 'Element') { - const sapperNoScroll = attr.name === 'sapper:noscroll'; - //skip Attribute shorthand, that is handled below - if ( - (attr.value !== true && - !( - attr.value.length && - attr.value.length == 1 && - attr.value[0].type == 'AttributeShorthand' - )) || - sapperNoScroll - ) { - let name = attr.name; - if (!svgAttributes.find((x) => x == name)) { - name = name.toLowerCase(); - } - - //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute - const colonIndex = name.indexOf(':'); - if (colonIndex >= 0) { - const parts = name.split(':'); - name = parts[0] + parts[1][0].toUpperCase() + parts[1].substring(1); - } - - str.overwrite(attr.start, attr.start + attr.name.length, name); - - transformedFromDirectiveOrNamespace = true; - } - } - - //we are a bare attribute - if (attr.value === true) { - if ( - parent.type === 'Element' && - !transformedFromDirectiveOrNamespace && - parent.name !== '!DOCTYPE' - ) { - str.overwrite(attr.start, attr.end, attr.name.toLowerCase()); - } - return; - } - - if (attr.value.length == 0) return; //wut? - //handle single value - if (attr.value.length == 1) { - const attrVal = attr.value[0]; - - if (attr.name == 'slot') { - str.remove(attr.start, attr.end); - return; - } - - if (attrVal.type == 'AttributeShorthand') { - let attrName = attrVal.expression.name; - if (parent.type == 'Element') { - // eslint-disable-next-line max-len - attrName = svgAttributes.find((a) => a == attrName) - ? attrName - : attrName.toLowerCase(); - } - - str.appendRight(attr.start, `${attrName}=`); - return; - } - - const equals = htmlx.lastIndexOf('=', attrVal.start); - if (attrVal.type == 'Text') { - const endsWithQuote = - htmlx.lastIndexOf('"', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf("'", attrVal.end) === attrVal.end - 1; - const needsQuotes = attrVal.end == attr.end && !endsWithQuote; - - const hasBrackets = - htmlx.lastIndexOf('}', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf('}"', attrVal.end) === attrVal.end - 1 || - htmlx.lastIndexOf("}'", attrVal.end) === attrVal.end - 1; - const needsNumberConversion = - !hasBrackets && - parent.type === 'Element' && - numberOnlyAttributes.has(attr.name.toLowerCase()) && - !isNaN(attrVal.data); - - if (needsNumberConversion) { - if (needsQuotes) { - str.prependRight(equals + 1, '{'); - str.appendLeft(attr.end, '}'); - } else { - str.overwrite(equals + 1, equals + 2, '{'); - str.overwrite(attr.end - 1, attr.end, '}'); - } - } else if (needsQuotes) { - str.prependRight(equals + 1, '"'); - str.appendLeft(attr.end, '"'); - } - return; - } - - if (attrVal.type == 'MustacheTag') { - //if the end doesn't line up, we are wrapped in quotes - if (attrVal.end != attr.end) { - str.remove(attrVal.start - 1, attrVal.start); - str.remove(attr.end - 1, attr.end); - } - return; - } - return; - } - - // we have multiple attribute values, so we build a string out of them. - // technically the user can do something funky like attr="text "{value} or even attr=text{value} - // so instead of trying to maintain a nice sourcemap with prepends etc, we just overwrite the whole thing - - const equals = htmlx.lastIndexOf('=', attr.value[0].start); - str.overwrite(equals, attr.value[0].start, '={`'); - - for (const n of attr.value) { - if (n.type == 'MustacheTag') { - str.appendRight(n.start, '$'); - } - } - - if (htmlx[attr.end - 1] == '"') { - str.overwrite(attr.end - 1, attr.end, '`}'); - } else { - str.appendLeft(attr.end, '`}'); - } - }; - - const handleElement = (node: Node) => { - //we just have to self close void tags since jsx always wants the /> - const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split( - ',', - ); - if (voidTags.find((x) => x == node.name)) { - if (htmlx[node.end - 2] != '/') { - str.appendRight(node.end - 1, '/'); - } - } - - //some tags auto close when they encounter certain elements, jsx doesn't support this - if (htmlx[node.end - 1] != '>') { - str.appendRight(node.end, ``); - } - }; - - const handleIf = (ifBlock: Node) => { - if (ifBlock.elseif) { - //we are an elseif so our work is easier - str.appendLeft(ifBlock.expression.start, '('); - str.appendLeft(ifBlock.expression.end, ')'); - return; - } - // {#if expr} -> - // {() => { if (expr){ <> - str.overwrite(ifBlock.start, ifBlock.expression.start, '{() => {if ('); - const end = htmlx.indexOf('}', ifBlock.expression.end); - str.overwrite(ifBlock.expression.end, end + 1, '){<>'); - - // {/if} -> }}} - const endif = htmlx.lastIndexOf('{', ifBlock.end); - str.overwrite(endif, ifBlock.end, '}}}'); - }; - - // {:else} -> } else {<> - const handleElse = (elseBlock: Node, parent: Node) => { - if (parent.type != 'IfBlock') return; - const elseEnd = htmlx.lastIndexOf('}', elseBlock.start); - const elseword = htmlx.lastIndexOf(':else', elseEnd); - const elseStart = htmlx.lastIndexOf('{', elseword); - str.overwrite(elseStart, elseStart + 1, '}'); - str.overwrite(elseEnd, elseEnd + 1, '{<>'); - const colon = htmlx.indexOf(':', elseword); - str.remove(colon, colon + 1); - }; - - const handleEach = (eachBlock: Node) => { - // {#each items as item,i (key)} -> - // {__sveltets_each(items, (item,i) => (key) && <> - str.overwrite(eachBlock.start, eachBlock.expression.start, '{__sveltets_each('); - str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', ('); - - // {#each true, items as item} - if (eachBlock.expression.type === 'SequenceExpression') { - str.appendRight(eachBlock.expression.start, '('); - str.appendLeft(eachBlock.expression.end, ')'); - } - - let contextEnd = eachBlock.context.end; - if (eachBlock.index) { - const idxLoc = htmlx.indexOf(eachBlock.index, contextEnd); - contextEnd = idxLoc + eachBlock.index.length; - } - str.prependLeft(contextEnd, ') =>'); - if (eachBlock.key) { - const endEachStart = htmlx.indexOf('}', eachBlock.key.end); - str.overwrite(endEachStart, endEachStart + 1, ' && <>'); - } else { - const endEachStart = htmlx.indexOf('}', contextEnd); - str.overwrite(endEachStart, endEachStart + 1, ' <>'); - } - const endEach = htmlx.lastIndexOf('{', eachBlock.end); - // {/each} -> )} or {:else} -> )} - if (eachBlock.else) { - const elseEnd = htmlx.lastIndexOf('}', eachBlock.else.start); - const elseStart = htmlx.lastIndexOf('{', elseEnd); - str.overwrite(elseStart, elseEnd + 1, ')}'); - str.remove(endEach, eachBlock.end); - } else { - str.overwrite(endEach, eachBlock.end, ')}'); - } - }; - - const handleComment = (node: Node) => { - str.remove(node.start, node.end); - }; - - const handleSvelteTag = (node: Node) => { - const colon = htmlx.indexOf(':', node.start); - str.remove(colon, colon + 1); - - const closeTag = htmlx.lastIndexOf('/' + node.name, node.end); - if (closeTag > node.start) { - const colon = htmlx.indexOf(':', closeTag); - str.remove(colon, colon + 1); - } - }; (svelte as any).walk(ast, { enter: (node: Node, parent: Node, prop: string, index: number) => { try { switch (node.type) { case 'IfBlock': - handleIf(node); + handleIf(htmlx, str, node); break; case 'EachBlock': - handleEach(node); + handleEach(htmlx, str, node); break; case 'ElseBlock': - handleElse(node, parent); + handleElse(htmlx, str, node, parent); break; case 'AwaitBlock': handleAwait(htmlx, str, node); break; case 'RawMustacheTag': - handleRaw(node); + handleRawHtml(htmlx, str, node); break; case 'DebugTag': - handleDebug(node); + handleDebug(htmlx, str, node); break; case 'InlineComponent': - handleComponent(node); + handleComponent(htmlx, str, node); break; case 'Element': - handleElement(node); + handleElement(htmlx, str, node); break; case 'Comment': - handleComment(node); + handleComment(str, node); break; case 'Binding': - handleBinding(node, parent); + handleBinding(htmlx, str, node, parent); break; case 'Class': - handleClassDirective(node); + handleClassDirective(str, node); break; case 'Action': - handleActionDirective(node, parent); + handleActionDirective(htmlx, str, node, parent); break; case 'Transition': - handleTransitionDirective(node); + handleTransitionDirective(htmlx, str, node); break; case 'Animation': - handleAnimateDirective(node); + handleAnimateDirective(htmlx, str, node); break; case 'Attribute': - handleAttribute(node, parent); + handleAttribute(htmlx, str, node, parent); break; case 'EventHandler': - handleEventHandler(node, parent); + handleEventHandler(htmlx, str, node, parent); break; case 'Options': - handleSvelteTag(node); + handleSvelteTag(htmlx, str, node); break; case 'Window': - handleSvelteTag(node); + handleSvelteTag(htmlx, str, node); break; case 'Head': - handleSvelteTag(node); + handleSvelteTag(htmlx, str, node); break; case 'Body': - handleSvelteTag(node); + handleSvelteTag(htmlx, str, node); break; } - if (onWalk) onWalk(node, parent, prop, index); + if (onWalk) { + onWalk(node, parent, prop, index); + } } catch (e) { console.error('Error walking node ', node); throw e; @@ -658,7 +120,9 @@ export function convertHtmlxToJsx( leave: (node: Node, parent: Node, prop: string, index: number) => { try { - if (onLeave) onLeave(node, parent, prop, index); + if (onLeave) { + onLeave(node, parent, prop, index); + } } catch (e) { console.error('Error leaving node ', node); throw e; @@ -667,6 +131,9 @@ export function convertHtmlxToJsx( }); } +/** + * @internal For testing only + */ export function htmlx2jsx(htmlx: string) { const ast = parseHtmlx(htmlx); const str = new MagicString(htmlx); diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts new file mode 100644 index 000000000..42cc8e6fd --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/action-directive.ts @@ -0,0 +1,29 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * use:xxx ---> {...__sveltets_ensureAction(__sveltets_mapElementTag('ParentNodeName', xxx))} + */ +export function handleActionDirective( + htmlx: string, + str: MagicString, + attr: Node, + parent: Node, +): void { + str.overwrite( + attr.start, + attr.start + 'use:'.length, + `{...__sveltets_ensureAction(__sveltets_mapElementTag('${parent.name}'),`, + ); + + if (!attr.expression) { + str.appendLeft(attr.end, ')}'); + return; + } + + str.overwrite(attr.start + `use:${attr.name}`.length, attr.expression.start, ','); + str.appendLeft(attr.expression.end, ')'); + if (htmlx[attr.end - 1] == '"') { + str.remove(attr.end - 1, attr.end); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts new file mode 100644 index 000000000..be1b54b3f --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/animation-directive.ts @@ -0,0 +1,27 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * animation:xxx(yyy) ---> {...__sveltets_ensureAnimation(xxx, yyy)} + */ +export function handleAnimateDirective(htmlx: string, str: MagicString, attr: Node): void { + str.overwrite( + attr.start, + htmlx.indexOf(':', attr.start) + 1, + '{...__sveltets_ensureAnimation(', + ); + + if (!attr.expression) { + str.appendLeft(attr.end, ', {})}'); + return; + } + str.overwrite( + htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, + attr.expression.start, + ', ', + ); + str.appendLeft(attr.expression.end, ')'); + if (htmlx[attr.end - 1] == '"') { + str.remove(attr.end - 1, attr.end); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts new file mode 100644 index 000000000..9da86f3af --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts @@ -0,0 +1,167 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import svgAttributes from '../svgattributes'; + +/** + * List taken from `svelte-jsx.d.ts` by searching for all attributes of type number + */ +const numberOnlyAttributes = new Set([ + 'cols', + 'colspan', + 'currenttime', + 'defaultplaybackrate', + 'high', + 'low', + 'marginheight', + 'marginwidth', + 'minlength', + 'maxlength', + 'optimum', + 'rows', + 'rowspan', + 'size', + 'span', + 'start', + 'tabindex', + 'results', + 'volume', +]); + +/** + * Handle various kinds of attributes and make them conform to JSX. + * - {x} ---> x={x} + * - x="{..}" ---> x={..} + * - lowercase DOM attributes + * - multi-value handling + */ +export function handleAttribute(htmlx: string, str: MagicString, attr: Node, parent: Node): void { + let transformedFromDirectiveOrNamespace = false; + + //if we are on an "element" we are case insensitive, lowercase to match our JSX + if (parent.type == 'Element') { + const sapperNoScroll = attr.name === 'sapper:noscroll'; + //skip Attribute shorthand, that is handled below + if ( + (attr.value !== true && + !( + attr.value.length && + attr.value.length == 1 && + attr.value[0].type == 'AttributeShorthand' + )) || + sapperNoScroll + ) { + let name = attr.name; + if (!svgAttributes.find((x) => x == name)) { + name = name.toLowerCase(); + } + + //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute + const colonIndex = name.indexOf(':'); + if (colonIndex >= 0) { + const parts = name.split(':'); + name = parts[0] + parts[1][0].toUpperCase() + parts[1].substring(1); + } + + str.overwrite(attr.start, attr.start + attr.name.length, name); + + transformedFromDirectiveOrNamespace = true; + } + } + + //we are a bare attribute + if (attr.value === true) { + if ( + parent.type === 'Element' && + !transformedFromDirectiveOrNamespace && + parent.name !== '!DOCTYPE' + ) { + str.overwrite(attr.start, attr.end, attr.name.toLowerCase()); + } + return; + } + + if (attr.value.length == 0) return; //wut? + //handle single value + if (attr.value.length == 1) { + const attrVal = attr.value[0]; + + if (attr.name == 'slot') { + str.remove(attr.start, attr.end); + return; + } + + if (attrVal.type == 'AttributeShorthand') { + let attrName = attrVal.expression.name; + if (parent.type == 'Element') { + // eslint-disable-next-line max-len + attrName = svgAttributes.find((a) => a == attrName) + ? attrName + : attrName.toLowerCase(); + } + + str.appendRight(attr.start, `${attrName}=`); + return; + } + + const equals = htmlx.lastIndexOf('=', attrVal.start); + if (attrVal.type == 'Text') { + const endsWithQuote = + htmlx.lastIndexOf('"', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf("'", attrVal.end) === attrVal.end - 1; + const needsQuotes = attrVal.end == attr.end && !endsWithQuote; + + const hasBrackets = + htmlx.lastIndexOf('}', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf('}"', attrVal.end) === attrVal.end - 1 || + htmlx.lastIndexOf("}'", attrVal.end) === attrVal.end - 1; + const needsNumberConversion = + !hasBrackets && + parent.type === 'Element' && + numberOnlyAttributes.has(attr.name.toLowerCase()) && + !isNaN(attrVal.data); + + if (needsNumberConversion) { + if (needsQuotes) { + str.prependRight(equals + 1, '{'); + str.appendLeft(attr.end, '}'); + } else { + str.overwrite(equals + 1, equals + 2, '{'); + str.overwrite(attr.end - 1, attr.end, '}'); + } + } else if (needsQuotes) { + str.prependRight(equals + 1, '"'); + str.appendLeft(attr.end, '"'); + } + return; + } + + if (attrVal.type == 'MustacheTag') { + //if the end doesn't line up, we are wrapped in quotes + if (attrVal.end != attr.end) { + str.remove(attrVal.start - 1, attrVal.start); + str.remove(attr.end - 1, attr.end); + } + return; + } + return; + } + + // we have multiple attribute values, so we build a string out of them. + // technically the user can do something funky like attr="text "{value} or even attr=text{value} + // so instead of trying to maintain a nice sourcemap with prepends etc, we just overwrite the whole thing + + const equals = htmlx.lastIndexOf('=', attr.value[0].start); + str.overwrite(equals, attr.value[0].start, '={`'); + + for (const n of attr.value) { + if (n.type == 'MustacheTag') { + str.appendRight(n.start, '$'); + } + } + + if (htmlx[attr.end - 1] == '"') { + str.overwrite(attr.end - 1, attr.end, '`}'); + } else { + str.appendLeft(attr.end, '`}'); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/await-block.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts similarity index 100% rename from packages/svelte2tsx/src/htmlxtojsx/nodes/await-block.ts rename to packages/svelte2tsx/src/htmlxtojsx/nodes/await.ts diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts new file mode 100644 index 000000000..8d5f2c8b8 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/binding.ts @@ -0,0 +1,82 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import { isShortHandAttribute, getThisType } from '../utils/node-utils'; + +const oneWayBindingAttributes: Map = new Map( + ['clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight'] + .map((e) => [e, 'HTMLDivElement'] as [string, string]) + .concat( + ['duration', 'buffered', 'seekable', 'seeking', 'played', 'ended'].map((e) => [ + e, + 'HTMLMediaElement', + ]), + ), +); + +/** + * Transform bind:xxx into something that conforms to JSX + */ +export function handleBinding(htmlx: string, str: MagicString, attr: Node, el: Node): void { + //bind group on input + if (attr.name == 'group' && el.name == 'input') { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, '{...__sveltets_empty('); + + const endBrackets = ')}'; + if (isShortHandAttribute(attr)) { + str.prependRight(attr.end, endBrackets); + } else { + str.overwrite(attr.expression.end, attr.end, endBrackets); + } + return; + } + + const supportsBindThis = ['InlineComponent', 'Element', 'Body']; + + //bind this + if (attr.name === 'this' && supportsBindThis.includes(el.type)) { + const thisType = getThisType(el); + + if (thisType) { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, `{...__sveltets_ensureType(${thisType}, `); + str.overwrite(attr.expression.end, attr.end, ')}'); + return; + } + } + + //one way binding + if (oneWayBindingAttributes.has(attr.name) && el.type === 'Element') { + str.remove(attr.start, attr.expression.start); + str.appendLeft(attr.expression.start, `{...__sveltets_empty(`); + if (isShortHandAttribute(attr)) { + // eslint-disable-next-line max-len + str.appendLeft( + attr.end, + `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}`, + ); + } else { + // eslint-disable-next-line max-len + str.overwrite( + attr.expression.end, + attr.end, + `=__sveltets_instanceOf(${oneWayBindingAttributes.get(attr.name)}).${attr.name})}`, + ); + } + return; + } + + str.remove(attr.start, attr.start + 'bind:'.length); + if (attr.expression.start === attr.start + 'bind:'.length) { + str.prependLeft(attr.expression.start, `${attr.name}={`); + str.appendLeft(attr.end, `}`); + return; + } + + //remove possible quotes + if (htmlx[attr.end - 1] === '"') { + const firstQuote = htmlx.indexOf('"', attr.start); + str.remove(firstQuote, firstQuote + 1); + str.remove(attr.end - 1, attr.end); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts new file mode 100644 index 000000000..afe49eb03 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/class-directive.ts @@ -0,0 +1,15 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * class:xx={yyy} ---> {...__sveltets_ensureType(Boolean, !!(yyy))} + */ +export function handleClassDirective(str: MagicString, attr: Node): void { + str.overwrite(attr.start, attr.expression.start, `{...__sveltets_ensureType(Boolean, !!(`); + const endBrackets = `))}`; + if (attr.end !== attr.expression.end) { + str.overwrite(attr.expression.end, attr.end, endBrackets); + } else { + str.appendLeft(attr.end, endBrackets); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts new file mode 100644 index 000000000..949c512d1 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/comment.ts @@ -0,0 +1,9 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * Removes comment + */ +export function handleComment(str: MagicString, node: Node): void { + str.remove(node.start, node.end); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component-type.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component-type.ts deleted file mode 100644 index 55856783b..000000000 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/component-type.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Node } from 'estree-walker'; - -export function getTypeForComponent(node: Node): string { - if (node.name === 'svelte:component' || node.name === 'svelte:self') { - return '__sveltets_componentType()'; - } else { - return node.name; - } -} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts new file mode 100644 index 000000000..687ca8de7 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts @@ -0,0 +1,85 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import { getSlotName } from '../../utils/svelteAst'; +import { beforeStart } from '../utils/node-utils'; +import { getSingleSlotDef } from '../../nodes/slot'; + +/** + * Handle `` and slot-specific transformations. + */ +export function handleComponent(htmlx: string, str: MagicString, el: Node): void { + //we need to remove : if it is a svelte component + if (el.name.startsWith('svelte:')) { + const colon = htmlx.indexOf(':', el.start); + str.remove(colon, colon + 1); + + const closeTag = htmlx.lastIndexOf('/' + el.name, el.end); + if (closeTag > el.start) { + const colon = htmlx.indexOf(':', closeTag); + str.remove(colon, colon + 1); + } + } + + //we only need to do something if there is a let or slot + handleSlot(htmlx, str, el, el, 'default'); + + //walk the direct children looking for slots. We do this here because we need the name of our component for handleSlot + //we could lean on leave/enter, but I am lazy + if (!el.children) return; + for (const child of el.children) { + const slotName = getSlotName(child); + if (slotName) { + handleSlot(htmlx, str, child, el, slotName); + } + } +} + +function handleSlot( + htmlx: string, + str: MagicString, + slotEl: Node, + component: Node, + slotName: string, +): void { + //collect "let" definitions + let hasMoved = false; + let afterTag: number; + for (const attr of slotEl.attributes) { + if (attr.type != 'Let') { + continue; + } + + if (slotEl.children.length == 0) { + //no children anyway, just wipe out the attribute + str.remove(attr.start, attr.end); + continue; + } + + afterTag = afterTag || htmlx.lastIndexOf('>', slotEl.children[0].start) + 1; + + str.move(attr.start, attr.end, afterTag); + + //remove let: + if (hasMoved) { + str.overwrite(attr.start, attr.start + 'let:'.length, ', '); + } else { + str.remove(attr.start, attr.start + 'let:'.length); + } + hasMoved = true; + if (attr.expression) { + //overwrite the = as a : + const equalSign = htmlx.lastIndexOf('=', attr.expression.start); + const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); + str.overwrite(equalSign, curly + 1, ':'); + str.remove(attr.expression.end, attr.end); + } + } + if (!hasMoved) { + return; + } + str.appendLeft(afterTag, '{() => { let {'); + str.appendRight(afterTag, `} = ${getSingleSlotDef(component, slotName)}` + ';<>'); + + const closeTagStart = htmlx.lastIndexOf('<', slotEl.end); + str.appendLeft(closeTagStart, '}}'); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts new file mode 100644 index 000000000..caa73682a --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/debug.ts @@ -0,0 +1,10 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * {@debug ...} ---> {...} + */ +export function handleDebug(htmlx: string, str: MagicString, debugBlock: Node): void { + const tokenStart = htmlx.indexOf('@debug', debugBlock.start); + str.remove(tokenStart, tokenStart + '@debug'.length); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts new file mode 100644 index 000000000..2ca178288 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/each.ts @@ -0,0 +1,42 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * Transform each block into something JSX can understand. + */ +export function handleEach(htmlx: string, str: MagicString, eachBlock: Node): void { + // {#each items as item,i (key)} -> + // {__sveltets_each(items, (item,i) => (key) && <> + str.overwrite(eachBlock.start, eachBlock.expression.start, '{__sveltets_each('); + str.overwrite(eachBlock.expression.end, eachBlock.context.start, ', ('); + + // {#each true, items as item} + if (eachBlock.expression.type === 'SequenceExpression') { + str.appendRight(eachBlock.expression.start, '('); + str.appendLeft(eachBlock.expression.end, ')'); + } + + let contextEnd = eachBlock.context.end; + if (eachBlock.index) { + const idxLoc = htmlx.indexOf(eachBlock.index, contextEnd); + contextEnd = idxLoc + eachBlock.index.length; + } + str.prependLeft(contextEnd, ') =>'); + if (eachBlock.key) { + const endEachStart = htmlx.indexOf('}', eachBlock.key.end); + str.overwrite(endEachStart, endEachStart + 1, ' && <>'); + } else { + const endEachStart = htmlx.indexOf('}', contextEnd); + str.overwrite(endEachStart, endEachStart + 1, ' <>'); + } + const endEach = htmlx.lastIndexOf('{', eachBlock.end); + // {/each} -> )} or {:else} -> )} + if (eachBlock.else) { + const elseEnd = htmlx.lastIndexOf('}', eachBlock.else.start); + const elseStart = htmlx.lastIndexOf('{', elseEnd); + str.overwrite(elseStart, elseEnd + 1, ')}'); + str.remove(endEach, eachBlock.end); + } else { + str.overwrite(endEach, eachBlock.end, ')}'); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts new file mode 100644 index 000000000..0da8d94e8 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts @@ -0,0 +1,22 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * Special treatment for self-closing / void tags to make them conform to JSX. + */ +export function handleElement(htmlx: string, str: MagicString, node: Node): void { + //we just have to self close void tags since jsx always wants the /> + const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split( + ',', + ); + if (voidTags.find((x) => x == node.name)) { + if (htmlx[node.end - 2] != '/') { + str.appendRight(node.end - 1, '/'); + } + } + + //some tags auto close when they encounter certain elements, jsx doesn't support this + if (htmlx[node.end - 1] != '>') { + str.appendRight(node.end, ``); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts new file mode 100644 index 000000000..903af4c92 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/event-handler.ts @@ -0,0 +1,50 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import { getTypeForComponent } from '../utils/node-utils'; + +/** + * Transform on:xxx={yyy} + * - For DOM elements: ---> onxxx={yyy} + * - For Svelte components/special elements: ---> {__sveltets_instanceOf(..ComponentType..).$on("xxx", yyy)} + */ +export function handleEventHandler( + htmlx: string, + str: MagicString, + attr: Node, + parent: Node, +): void { + const jsxEventName = attr.name; + + if ( + ['Element', 'Window', 'Body'].includes( + parent.type, + ) /*&& KnownEvents.indexOf('on'+jsxEventName) >= 0*/ + ) { + if (attr.expression) { + const endAttr = htmlx.indexOf('=', attr.start); + str.overwrite(attr.start + 'on:'.length - 1, endAttr, jsxEventName); + if (htmlx[attr.end - 1] == '"') { + const firstQuote = htmlx.indexOf('"', endAttr); + str.remove(firstQuote, firstQuote + 1); + str.remove(attr.end - 1, attr.end); + } + } else { + str.overwrite(attr.start + 'on:'.length - 1, attr.end, `${jsxEventName}={undefined}`); + } + } else { + if (attr.expression) { + const on = 'on'; + //for handler assignment, we change it to call to our __sveltets_ensureFunction + str.appendRight(attr.start, `{__sveltets_instanceOf(${getTypeForComponent(parent)}).$`); + const eventNameIndex = htmlx.indexOf(':', attr.start) + 1; + str.overwrite(htmlx.indexOf(on, attr.start) + on.length, eventNameIndex, `('`); + const eventEnd = htmlx.lastIndexOf('=', attr.expression.start); + str.overwrite(eventEnd, attr.expression.start, `', `); + str.overwrite(attr.expression.end, attr.end, ')}'); + str.move(attr.start, attr.end, parent.end); + } else { + //for passthrough handlers, we just remove + str.remove(attr.start, attr.end); + } + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts new file mode 100644 index 000000000..b2e76f973 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/if-else.ts @@ -0,0 +1,39 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * {# if ...}...{/if} ---> {() => {if(...){<>...}}} + */ +export function handleIf(htmlx: string, str: MagicString, ifBlock: Node): void { + if (ifBlock.elseif) { + //we are an elseif so our work is easier + str.appendLeft(ifBlock.expression.start, '('); + str.appendLeft(ifBlock.expression.end, ')'); + return; + } + // {#if expr} -> + // {() => { if (expr){ <> + str.overwrite(ifBlock.start, ifBlock.expression.start, '{() => {if ('); + const end = htmlx.indexOf('}', ifBlock.expression.end); + str.overwrite(ifBlock.expression.end, end + 1, '){<>'); + + // {/if} -> }}} + const endif = htmlx.lastIndexOf('{', ifBlock.end); + str.overwrite(endif, ifBlock.end, '}}}'); +} + +/** + * {:else} ---> } else {<> + */ +export function handleElse(htmlx: string, str: MagicString, elseBlock: Node, parent: Node): void { + if (parent.type !== 'IfBlock') { + return; + } + const elseEnd = htmlx.lastIndexOf('}', elseBlock.start); + const elseword = htmlx.lastIndexOf(':else', elseEnd); + const elseStart = htmlx.lastIndexOf('{', elseword); + str.overwrite(elseStart, elseStart + 1, '}'); + str.overwrite(elseEnd, elseEnd + 1, '{<>'); + const colon = htmlx.indexOf(':', elseword); + str.remove(colon, colon + 1); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts new file mode 100644 index 000000000..051d1c3b0 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/raw-html.ts @@ -0,0 +1,10 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * {@html ...} ---> {...} + */ +export function handleRawHtml(htmlx: string, str: MagicString, rawBlock: Node): void { + const tokenStart = htmlx.indexOf('@html', rawBlock.start); + str.remove(tokenStart, tokenStart + '@html'.length); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts new file mode 100644 index 000000000..19f0c063c --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts @@ -0,0 +1,17 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * `...` ----> `...` + * (same for :head, :body, :options) + */ +export function handleSvelteTag(htmlx: string, str: MagicString, node: Node): void { + const colon = htmlx.indexOf(':', node.start); + str.remove(colon, colon + 1); + + const closeTag = htmlx.lastIndexOf('/' + node.name, node.end); + if (closeTag > node.start) { + const colon = htmlx.indexOf(':', closeTag); + str.remove(colon, colon + 1); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts new file mode 100644 index 000000000..d1edf86bb --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/transition-directive.ts @@ -0,0 +1,33 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; + +/** + * transition:xxx(yyy) ---> {...__sveltets_ensureTransition(xxx, yyy)} + */ +export function handleTransitionDirective(htmlx: string, str: MagicString, attr: Node): void { + str.overwrite( + attr.start, + htmlx.indexOf(':', attr.start) + 1, + '{...__sveltets_ensureTransition(', + ); + + if (attr.modifiers.length) { + const local = htmlx.indexOf('|', attr.start); + str.remove(local, attr.expression ? attr.expression.start : attr.end); + } + + if (!attr.expression) { + str.appendLeft(attr.end, ', {})}'); + return; + } + + str.overwrite( + htmlx.indexOf(':', attr.start) + 1 + `${attr.name}`.length, + attr.expression.start, + ', ', + ); + str.appendLeft(attr.expression.end, ')'); + if (htmlx[attr.end - 1] == '"') { + str.remove(attr.end - 1, attr.end); + } +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts new file mode 100644 index 000000000..c9b9dd9f6 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/utils/node-utils.ts @@ -0,0 +1,28 @@ +import { Node } from 'estree-walker'; + +export function getTypeForComponent(node: Node): string { + if (node.name === 'svelte:component' || node.name === 'svelte:self') { + return '__sveltets_componentType()'; + } else { + return node.name; + } +} + +export function getThisType(node: Node): string | undefined { + switch (node.type) { + case 'InlineComponent': + return getTypeForComponent(node); + case 'Element': + return `__sveltets_ctorOf(__sveltets_mapElementTag('${node.name}'))`; + case 'Body': + return 'HTMLBodyElement'; + } +} + +export function beforeStart(start: number): number { + return start - 1; +} + +export function isShortHandAttribute(attr: Node): boolean { + return attr.expression.end === attr.end; +} diff --git a/packages/svelte2tsx/src/nodes/slot.ts b/packages/svelte2tsx/src/nodes/slot.ts index b87e929de..9fd5535d6 100644 --- a/packages/svelte2tsx/src/nodes/slot.ts +++ b/packages/svelte2tsx/src/nodes/slot.ts @@ -10,7 +10,7 @@ import { } from '../utils/svelteAst'; import TemplateScope from './TemplateScope'; import { SvelteIdentifier, WithName, BaseDirective } from '../interfaces'; -import { getTypeForComponent } from '../htmlxtojsx/nodes/component-type'; +import { getTypeForComponent } from '../htmlxtojsx/utils/node-utils'; function attributeStrValueAsJsExpression(attr: Node): string { if (attr.value.length == 0) return "''"; //wut?