Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a parent property to AST nodes #82

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,34 @@ console.log(parser(html)) // Logs a PostHTML AST
href: '#'
},
content: [
'\n ',
{
text: '\n ',
parent: [Circular]
},
{
tag: 'span',
attrs: {
class: 'animals__cat',
style: 'background: url(cat.png)'
},
content: ['Cat']
content: [{
text: 'Cat',
parent: [Circular],
}],
parent: [Circular]
},
'\n'
]
{
text: '\n',
parent: [Circular]
}
],
parent: [Circular]
}]
```

## PostHTML AST Format

Any parser being used with PostHTML should return a standard PostHTML [Abstract Syntax Tree](https://www.wikiwand.com/en/Abstract_syntax_tree) (AST). Fortunately, this is a very easy format to produce and understand. The AST is an array that can contain strings and objects. Any strings represent plain text content to be written to the output. Any objects represent HTML tags.
Any parser being used with PostHTML should return a standard PostHTML [Abstract Syntax Tree](https://www.wikiwand.com/en/Abstract_syntax_tree) (AST). Fortunately, this is a very easy format to produce and understand. The AST is an array that can contain two types of objects: tag objects and text node objects.

Tag objects generally look something like this:

Expand All @@ -72,11 +83,17 @@ Tag objects generally look something like this:
attrs: {
class: 'foo'
},
content: ['hello world!']
content: [{
text: 'hello world!',
parent: [Circular]
}],
parent: [Circular]
}
```

Tag objects can contain three keys. The `tag` key takes the name of the tag as the value. This can include custom tags. The optional `attrs` key takes an object with key/value pairs representing the attributes of the html tag. A boolean attribute has an empty string as its value. Finally, the optional `content` key takes an array as its value, which is a PostHTML AST. In this manner, the AST is a tree that should be walked recursively.
Tag objects can contain four keys. The `tag` key takes the name of the tag as the value. This can include custom tags. The optional `attrs` key takes an object with key/value pairs representing the attributes of the html tag. A boolean attribute has an empty string as its value. The optional `content` key takes an array as its value, which is a PostHTML AST. Finally, the optional `parent` key stores a reference to the parent node. In this manner, the AST is a tree that should be walked recursively.

Text node objects can contain two keys. The `text` key contains the text itself and the optional `parent` key stores a reference to the parent node.

## Options

Expand Down
51 changes: 34 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export type Tag = string | boolean;
export type Attributes = Record<string, string | number | boolean>;
export type Content = NodeText | Array<Node | Node[]>;

export type NodeText = string | number;
export type NodeText = { text: string | number };
export type NodeTag = {
tag?: Tag;
attrs?: Attributes;
content?: Content;
location?: SourceLocation;
};

export type Node = NodeText | NodeTag;
export type Node = (NodeText | NodeTag) & { parent?: NodeTag | Node[] };

const defaultOptions: ParserOptions = {
lowerCaseTags: false,
Expand All @@ -49,6 +49,21 @@ export const parser = (html: string, options: Options = {}): Node[] => {
return bufArray[bufArray.length - 1];
}

function appendChild(parent: NodeTag | Node[], child: Node) {
child.parent = parent;

if (Array.isArray(parent)) {
parent.push(child);
return;
}

if (!Array.isArray(parent.content)) {
parent.content = [];
}

parent.content.push(child);
}

function isDirective(directive: Directive, tag: string): boolean {
if (directive.name instanceof RegExp) {
const regex = new RegExp(directive.name.source, 'i');
Expand Down Expand Up @@ -84,17 +99,17 @@ export const parser = (html: string, options: Options = {}): Node[] => {

if (isDirective(directive, name.toLowerCase())) {
if (last === undefined) {
results.push(directiveText);
appendChild(results, { text: directiveText });
return;
}

if (typeof last === 'object') {
if ((typeof last === 'object') && !('text' in last)) {
if (last.content === undefined) {
last.content = [];
}

if (Array.isArray(last.content)) {
last.content.push(directiveText);
appendChild(last, { text: directiveText });
}
}
}
Expand All @@ -106,17 +121,17 @@ export const parser = (html: string, options: Options = {}): Node[] => {
const comment = `<!--${data}-->`;

if (last === undefined) {
results.push(comment);
appendChild(results, { text: comment });
return;
}

if (typeof last === 'object') {
if ((typeof last === 'object') && !('text' in last)) {
if (last.content === undefined) {
last.content = [];
}

if (Array.isArray(last.content)) {
last.content.push(comment);
appendChild(last, { text: comment });
}
}
}
Expand All @@ -142,25 +157,26 @@ export const parser = (html: string, options: Options = {}): Node[] => {
function onclosetag() {
const buf: Node | undefined = bufArray.pop();

if (buf && typeof buf === 'object' && buf.location && parser.endIndex !== null) {
if (buf && typeof buf === 'object' && !('text' in buf) &&
buf.location && parser.endIndex !== null) {
buf.location.end = locationTracker.getPosition(parser.endIndex);
}

if (buf) {
const last = bufferArrayLast();

if (bufArray.length <= 0) {
results.push(buf);
appendChild(results, buf);
return;
}

if (typeof last === 'object') {
if ((typeof last === 'object') && !('text' in last)) {
if (last.content === undefined) {
last.content = [];
}

if (Array.isArray(last.content)) {
last.content.push(buf);
appendChild(last, buf);
}
}
}
Expand All @@ -170,15 +186,16 @@ export const parser = (html: string, options: Options = {}): Node[] => {
const last: Node = bufferArrayLast();

if (last === undefined) {
results.push(text);
appendChild(results, { text });
return;
}

if (typeof last === 'object') {
if ((typeof last === 'object') && !('text' in last)) {
if (last.content && Array.isArray(last.content) && last.content.length > 0) {
const lastContentNode = last.content[last.content.length - 1];
if (typeof lastContentNode === 'string' && !lastContentNode.startsWith('<!--')) {
last.content[last.content.length - 1] = `${lastContentNode}${text}`;
if (('text' in lastContentNode) &&
typeof lastContentNode.text === 'string' && !lastContentNode.text.startsWith('<!--')) {
lastContentNode.text += String(text);
return;
}
}
Expand All @@ -188,7 +205,7 @@ export const parser = (html: string, options: Options = {}): Node[] => {
}

if (Array.isArray(last.content)) {
last.content.push(text);
appendChild(last, { text });
}
}
}
Expand Down
Loading