Browse Source

Add convertAttributeValues to allow parsing HTML attributes values (#6741)

option-getcss-empty-rules
Artur Arseniev 4 months ago
committed by GitHub
parent
commit
054fd9354d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      packages/core/src/parser/config/config.ts
  2. 1
      packages/core/src/parser/index.ts
  3. 52
      packages/core/src/parser/model/ParserHtml.ts
  4. 78
      packages/core/test/specs/parser/model/ParserHtml.ts

17
packages/core/src/parser/config/config.ts

@ -14,6 +14,11 @@ export type CustomParserCss = (input: string, editor: Editor) => ParsedCssRule[]
export type CustomParserHtml = (input: string, options: HTMLParserOptions) => HTMLElement;
export type ConvertAttributeValuesOption =
| boolean
| readonly string[]
| ((props: { attribute: string; value: string | boolean; node: HTMLElement }) => boolean);
export interface HTMLParseResult {
html: ComponentDefinitionDefined | ComponentDefinitionDefined[];
css?: CssRuleJSON[];
@ -83,6 +88,17 @@ export interface HTMLParserOptions extends OptionAsDocument {
* @default false
*/
convertDataGjsAttributesHyphens?: boolean;
/**
* Convert regular HTML attribute values using the same parser used by `data-gjs-*` attributes.
*
* - `true`: converts all regular attributes.
* - `string[]`: converts only the listed attributes, matched by exact attribute name.
* - `Function`: converts attributes when the function returns `true`.
*
* @default false
*/
convertAttributeValues?: ConvertAttributeValuesOption;
}
export interface ParserConfig {
@ -133,6 +149,7 @@ const config: () => ParserConfig = () => ({
allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
convertDataGjsAttributesHyphens: false,
convertAttributeValues: false,
},
});

1
packages/core/src/parser/index.ts

@ -63,6 +63,7 @@ export default class ParserModule extends Module<ParserConfig & { name?: string
* @param {Boolean|Function} [options.detectDocument] Indicate if or how to detect if the HTML string should be treated as document
* @param {Function} [options.preParser] How to pre-process the HTML string before parsing
* @param {Boolean} [options.convertDataGjsAttributesHyphens=false] Convert `data-gjs-*` attributes from hyphenated to camelCase (eg. `data-gjs-my-component` to `data-gjs-myComponent`)
* @param {Boolean|Array<String>|Function} [options.convertAttributeValues=false] Convert regular HTML attribute values using the same parser used by `data-gjs-*` attributes
* @returns {Object} Object containing the result `{ html: ..., css: ... }`
* @example
* const resHtml = Parser.parseHtml(`<table><div>Hi</div></table>`, {

52
packages/core/src/parser/model/ParserHtml.ts

@ -16,8 +16,11 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
modelAttrStart,
getPropAttribute(attrName: string, attrValue?: string) {
const name = attrName.replace(this.modelAttrStart, '');
parseAttributeValue(attrValue?: string | boolean) {
if (typeof attrValue !== 'string') {
return attrValue;
}
const valueLen = attrValue?.length || 0;
const firstChar = attrValue?.substring(0, 1);
const lastChar = attrValue?.substring(valueLen - 1);
@ -31,12 +34,38 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
(firstChar == '{' && lastChar == '}') || (firstChar == '[' && lastChar == ']') ? JSON.parse(value) : value;
} catch (e) {}
return value;
},
getPropAttribute(attrName: string, attrValue?: string) {
const name = attrName.replace(this.modelAttrStart, '');
const value = this.parseAttributeValue(attrValue);
return {
name,
value,
};
},
shouldConvertAttributeValue(
attribute: string,
value: string | boolean,
node: HTMLElement,
convertAttributeValues: HTMLParserOptions['convertAttributeValues'],
) {
if (!convertAttributeValues) {
return false;
} else if (convertAttributeValues === true) {
return true;
} else if (isArray(convertAttributeValues)) {
return convertAttributeValues.includes(attribute);
} else if (isFunction(convertAttributeValues)) {
return !!convertAttributeValues({ attribute, value, node });
}
return false;
},
/**
* Extract component props from an attribute object
* @param {Object} attr
@ -126,18 +155,23 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
return result;
},
parseNodeAttr(node: HTMLElement, modelResult?: ComponentDefinitionDefined) {
parseNodeAttr(
node: HTMLElement,
modelResult?: ComponentDefinitionDefined,
opts: HTMLParserOptions = config.optionsHtml || {},
) {
const model = modelResult || {};
const attrs = node.attributes || [];
const attrsLen = attrs.length;
const convertHyphens = !!config?.optionsHtml?.convertDataGjsAttributesHyphens;
const convertHyphens = !!opts.convertDataGjsAttributesHyphens;
const { convertAttributeValues } = opts;
const defaults =
(convertHyphens && !!model.type && result(em?.Components.getType(model.type)?.model.prototype, 'defaults')) ||
{};
for (let i = 0; i < attrsLen; i++) {
let nodeName = attrs[i].nodeName;
let nodeValue: string | boolean = attrs[i].nodeValue!;
let nodeValue: any = attrs[i].nodeValue!;
if (nodeName == 'style') {
model.style = this.parseStyle(nodeValue);
@ -160,6 +194,10 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
nodeValue = true;
}
if (this.shouldConvertAttributeValue(nodeName, nodeValue, node, convertAttributeValues)) {
nodeValue = this.parseAttributeValue(nodeValue);
}
if (!model.attributes) {
model.attributes = {};
}
@ -212,7 +250,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
model.tagName = tag && ns === 'http://www.w3.org/1999/xhtml' ? tag.toLowerCase() : tag;
}
model = this.parseNodeAttr(node, model);
model = this.parseNodeAttr(node, model, opts);
// Check for custom void elements (valid in XML)
if (!nodesLen && `${node.outerHTML}`.slice(-2) === '/>') {
@ -384,7 +422,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
if (asDocument) {
res.head = this.parseNode(docEl.head, cf);
res.root = this.parseNodeAttr(root);
res.root = this.parseNodeAttr(root, undefined, cf);
resHtml = this.parseNode(docEl.body, cf);
} else {
const result = this.parseNodes(root, cf);

78
packages/core/test/specs/parser/model/ParserHtml.ts

@ -823,6 +823,84 @@ describe('ParserHtml', () => {
});
});
describe('with convertAttributeValues', () => {
test('keeps regular attribute values as strings by default', () => {
const str = `<div data-bool="true" data-list="[1,2,3]" data-obj='{"key":"value"}' data-gjs-test='{"key":"value"}'></div>`;
const result = [
{
tagName: 'div',
test: { key: 'value' },
attributes: {
'data-bool': 'true',
'data-list': '[1,2,3]',
'data-obj': '{"key":"value"}',
},
},
];
expect(obj.parse(str).html).toEqual(result);
});
test('converts all regular attribute values when true', () => {
const str = `<div data-bool="true" data-false="false" data-list="[1,2,3]" data-obj='{"key":"value"}'></div>`;
const result = [
{
tagName: 'div',
attributes: {
'data-bool': true,
'data-false': false,
'data-list': [1, 2, 3],
'data-obj': { key: 'value' },
},
},
];
expect(obj.parse(str, null, { convertAttributeValues: true }).html).toEqual(result);
});
test('converts only exact attribute names when an array is provided', () => {
const str = `<img src='["image.png"]' srcset='["image@2x.png"]' data-test="false"/>`;
const result = [
{
tagName: 'img',
type: 'image',
attributes: {
src: ['image.png'],
srcset: '["image@2x.png"]',
'data-test': 'false',
},
},
];
expect(obj.parse(str, null, { convertAttributeValues: ['src'] }).html).toEqual(result);
});
test('converts attributes with a dynamic resolver function', () => {
const str = `<img src="[1,2,3]" alt="[1,2,3]"/><a href="[1,2,3]"></a>`;
const result = [
{
tagName: 'img',
type: 'image',
attributes: {
src: [1, 2, 3],
alt: '[1,2,3]',
},
},
{
tagName: 'a',
type: 'link',
attributes: {
href: '[1,2,3]',
},
},
];
expect(
obj.parse(str, null, {
convertAttributeValues: ({ attribute, value, node }) =>
attribute === 'src' && value === '[1,2,3]' && node.tagName.toLowerCase() === 'img',
}).html,
).toEqual(result);
});
});
describe('with convertDataGjsAttributesHyphens OFF (default)', () => {
beforeEach(() => {
em = new Editor({});

Loading…
Cancel
Save