mirror of https://github.com/artf/grapesjs.git
14 changed files with 592 additions and 26 deletions
@ -0,0 +1,105 @@ |
|||
import { ParsedNode, SyntheticElementCtor } from '../types'; |
|||
|
|||
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; |
|||
|
|||
const getTagName = (node: ParsedNode) => { |
|||
const tagName = node.tagName || ''; |
|||
return tagName && (!node.namespaceURI || node.namespaceURI === HTML_NAMESPACE) ? tagName.toUpperCase() : tagName; |
|||
}; |
|||
|
|||
const getNodeTextContent = (node: ParsedNode): string => { |
|||
if (node.nodeType === 3 || node.nodeType === 8) { |
|||
return node.textContent ?? ''; |
|||
} |
|||
|
|||
if (node.textContent && !node.childNodes?.length) { |
|||
return node.textContent; |
|||
} |
|||
|
|||
return (node.childNodes || []).map(getNodeTextContent).join(''); |
|||
}; |
|||
|
|||
const getClassList = (node: ParsedNode) => { |
|||
const value = node.attributes?.class || ''; |
|||
const items = value.split(/\s+/).filter(Boolean); |
|||
|
|||
return { |
|||
value, |
|||
length: items.length, |
|||
contains(name: string) { |
|||
return items.includes(name); |
|||
}, |
|||
item(index: number) { |
|||
return items[index] || null; |
|||
}, |
|||
toString() { |
|||
return value; |
|||
}, |
|||
}; |
|||
}; |
|||
|
|||
export class SyntheticElement { |
|||
node: ParsedNode; |
|||
parent?: SyntheticElement; |
|||
|
|||
constructor(node: ParsedNode, parent?: SyntheticElement) { |
|||
this.node = node; |
|||
this.parent = parent; |
|||
} |
|||
|
|||
get nodeType() { |
|||
return this.node.nodeType; |
|||
} |
|||
|
|||
get tagName() { |
|||
return getTagName(this.node); |
|||
} |
|||
|
|||
get nodeName() { |
|||
return this.tagName; |
|||
} |
|||
|
|||
get namespaceURI() { |
|||
return this.node.namespaceURI; |
|||
} |
|||
|
|||
get parentNode() { |
|||
return this.parent; |
|||
} |
|||
|
|||
get childNodes() { |
|||
return (this.node.childNodes || []).map((node) => new (this.constructor as SyntheticElementCtor)(node, this)); |
|||
} |
|||
|
|||
get children() { |
|||
return this.childNodes.filter((node) => node.nodeType === 1); |
|||
} |
|||
|
|||
get textContent() { |
|||
return getNodeTextContent(this.node); |
|||
} |
|||
|
|||
get nodeValue() { |
|||
return this.nodeType === 3 || this.nodeType === 8 ? this.textContent : null; |
|||
} |
|||
|
|||
get className() { |
|||
return this.getAttribute('class') || ''; |
|||
} |
|||
|
|||
get classList() { |
|||
return getClassList(this.node); |
|||
} |
|||
|
|||
getAttribute(key: string) { |
|||
return this.node.attributes?.[key]; |
|||
} |
|||
|
|||
hasAttribute(key: string) { |
|||
return Object.prototype.hasOwnProperty.call(this.node.attributes || {}, key); |
|||
} |
|||
} |
|||
|
|||
export const getSyntheticElementCtor = ( |
|||
customSyntheticElement?: ((SyntheticElement: SyntheticElementCtor) => SyntheticElementCtor) | null, |
|||
) => (customSyntheticElement ? customSyntheticElement(SyntheticElement as SyntheticElementCtor) : SyntheticElement); |
|||
@ -0,0 +1,75 @@ |
|||
import Editor from '../../../src/editor/model/Editor'; |
|||
|
|||
describe('Parser module', () => { |
|||
let em: Editor; |
|||
|
|||
beforeEach(() => { |
|||
em = new Editor({}); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
em.destroy(); |
|||
}); |
|||
|
|||
test('adds, gets, removes code parsers and selects them by default', () => { |
|||
const parse = jest.fn(() => []); |
|||
const parser = em.Parser.addParserCode('test-parser', parse); |
|||
|
|||
expect(parser).toEqual({ |
|||
id: 'test-parser', |
|||
parse, |
|||
}); |
|||
expect(em.Parser.getParserCode('test-parser')).toBe(parser); |
|||
expect(em.Parser.parserCode).toBe('test-parser'); |
|||
expect(em.Parser.removeParserCode('test-parser')).toBe(parser); |
|||
expect(em.Parser.getParserCode('test-parser')).toBeUndefined(); |
|||
expect(em.Parser.parserCode).toBe(''); |
|||
}); |
|||
|
|||
test('loads code parsers from init config and selects the last one by default', () => { |
|||
const first = jest.fn(() => []); |
|||
const second = jest.fn(() => []); |
|||
const editor = new Editor({ |
|||
parser: { |
|||
parsersCode: { |
|||
first, |
|||
second, |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
expect(editor.Parser.getParserCode('first')?.parse).toBe(first); |
|||
expect(editor.Parser.getParserCode('second')?.parse).toBe(second); |
|||
expect(editor.Parser.parserCode).toBe('second'); |
|||
|
|||
editor.destroy(); |
|||
}); |
|||
|
|||
test('allows init config to skip selecting a default parser', () => { |
|||
const editor = new Editor({ |
|||
parser: { |
|||
parserCode: '', |
|||
parsersCode: { |
|||
first: () => [], |
|||
second: () => [], |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
expect(editor.Parser.parserCode).toBe(''); |
|||
|
|||
editor.destroy(); |
|||
}); |
|||
|
|||
test('supports global parserCode, per-call overrides, and legacy fallback', () => { |
|||
em.Parser.addParserCode('section-parser', () => [{ nodeType: 1, tagName: 'section' }]); |
|||
em.Parser.addParserCode('article-parser', () => [{ nodeType: 1, tagName: 'article' }], { skipSelect: true }); |
|||
|
|||
expect(em.Parser.parseHtml('<div></div>').html).toEqual({ tagName: 'section' }); |
|||
expect(em.Parser.parseHtml('<div></div>', { parserCode: 'article-parser' }).html).toEqual({ tagName: 'article' }); |
|||
expect(em.Parser.parseHtml('<div></div>', { parserCode: '' }).html).toEqual({ tagName: 'div' }); |
|||
expect(() => em.Parser.parseHtml('<div></div>', { parserCode: 'missing-parser' })).toThrow( |
|||
'Parser code "missing-parser" not found', |
|||
); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue