Skip to content

Commit

Permalink
feat: add support for serializing html attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Aug 8, 2023
1 parent 311be7d commit fdb5e3c
Show file tree
Hide file tree
Showing 5 changed files with 417 additions and 3 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@
"@poppinss/inspect": "^1.0.1",
"@poppinss/macroable": "^1.0.0-7",
"@poppinss/utils": "^6.5.0-5",
"classnames": "^2.3.2",
"edge-error": "^4.0.0-0",
"edge-lexer": "^6.0.0-4",
"edge-parser": "^9.0.0-2",
"he": "^1.2.0",
"js-stringify": "^1.0.2",
"stringify-attributes": "^4.0.0"
"property-information": "^6.2.0"
},
"author": "virk",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion src/component/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
*/

import lodash from '@poppinss/utils/lodash'
import stringifyAttributes from 'stringify-attributes'

import { htmlSafe } from '../template.js'
import { PropsContract } from '../types.js'
import { stringifyAttributes } from '../utils.js'

/**
* Class to ease interactions with component props
Expand Down
2 changes: 1 addition & 1 deletion src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import he from 'he'
import { EdgeError } from 'edge-error'
import lodash from '@poppinss/utils/lodash'
import Macroable from '@poppinss/macroable'
import stringifyAttributes from 'stringify-attributes'

import { Compiler } from './compiler.js'
import { Processor } from './processor.js'
import { Props } from './component/props.js'
import { stringifyAttributes } from './utils.js'
import type { CompiledTemplate } from './types.js'

/**
Expand Down
143 changes: 143 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,81 @@
* file that was distributed with this source code.
*/

// @ts-expect-error untyped module
import JSStringify from 'js-stringify'
import classNames from 'classnames'
import { EdgeError } from 'edge-error'
import type { TagToken } from 'edge-lexer/types'
import { find, html } from 'property-information'
import { expressions as expressionsList, Parser } from 'edge-parser'

type ExpressionList = readonly (keyof typeof expressionsList)[]

/**
* Function to register custom properties
* with "property-information" package.
*/
function definePropertyInformation(property: string, value?: any) {
html.normal[property] = property
html.property[property] = {
attribute: property,
boolean: true,
property: property,
space: 'html',
booleanish: false,
commaOrSpaceSeparated: false,
commaSeparated: false,
spaceSeparated: false,
number: false,
overloadedBoolean: false,
defined: false,
mustUseProperty: false,
...value,
}
}

definePropertyInformation('x-cloak')
definePropertyInformation('x-ignore')
definePropertyInformation('x-transition:enterstart', {
attribute: 'x-transition:enter-start',
property: 'x-transition:enterStart',
boolean: false,
spaceSeparated: true,
commaOrSpaceSeparated: true,
})
definePropertyInformation('x-transition:enterend', {
attribute: 'x-transition:enter-end',
property: 'x-transition:enterEnd',
boolean: false,
spaceSeparated: true,
commaOrSpaceSeparated: true,
})
definePropertyInformation('x-transition:leavestart', {
attribute: 'x-transition:leave-start',
property: 'x-transition:leaveStart',
boolean: false,
spaceSeparated: true,
commaOrSpaceSeparated: true,
})
definePropertyInformation('x-transition:leaveend', {
attribute: 'x-transition:leave-end',
property: 'x-transition:leaveEnd',
boolean: false,
spaceSeparated: true,
commaOrSpaceSeparated: true,
})

/**
* Alpine namespaces we handle with special
* rules when stringifying attributes
*/
const alpineNamespaces: Record<string, string> = {
x: 'x-',
xOn: 'x-on:',
xBind: 'x-bind:',
xTransition: 'x-transition:',
}

/**
* Raise an `E_UNALLOWED_EXPRESSION` exception. Filename and expression is
* required to point the error stack to the correct file
Expand Down Expand Up @@ -220,3 +289,77 @@ export class StringifiedObject {
return objectifyString.flush()
}
}

/**
* Stringify an object to props to HTML attributes
*/
export function stringifyAttributes(props: any, namespace?: string): string {
const attributes = Object.keys(props)
if (attributes.length === 0) {
return ''
}

return `${attributes
.reduce<string[]>((result, key) => {
let value = props[key]
key = namespace ? `${namespace}${key}` : key
/**
* No value defined, remove attribute
*/
if (!value) {
return result
}
/**
* Handle alpine properties separately
*/
if (alpineNamespaces[key] && typeof value === 'object') {
result = result.concat(stringifyAttributes(value, alpineNamespaces[key]))
return result
}
const propInfo = find(html, key)
const attribute = propInfo.attribute
/**
* Ignore svg elements and their attributes
*/
if (!propInfo || propInfo.space === 'svg') {
return result
}
/**
* Boolean properties
*/
if (propInfo.boolean) {
result.push(attribute)
return result
}
/**
* Encoding rules for certain properties.
*
* - Class values can be objects with conditionals
* - x-data as an object will be converted to a JSON value
* - Arrays will be concatenated into a string list and html escaped
* - Non-booleanish and numeric properties will be html escaped
*/
if (key === 'class') {
value = `"${classNames(value)}"`
} else if (key === 'x-data') {
value = typeof value === 'string' ? `"${value}"` : JSStringify(value)
} else if (Array.isArray(value)) {
value = `"${value.join(propInfo.commaSeparated ? ',' : ' ')}"`
} else if (!propInfo.booleanish && !propInfo.number) {
value = `"${String(value)}"`
}
/**
* Push attribute value string
*/
result.push(`${attribute}=${value}`)
return result
}, [])
.join(' ')}`
}
Loading

0 comments on commit fdb5e3c

Please sign in to comment.