Skip to content

Commit

Permalink
Fixes the type of the RenderableTreeNode (markdoc#249)
Browse files Browse the repository at this point in the history
* Tweaks type based on feedback

* Additional typing tweaks

* move isTag out of types

* fix types

* fix tests

* undo html renderer changes

* fix up types in tests

Co-authored-by: Mike Fix <[email protected]>
  • Loading branch information
rpaul-stripe and mfix-stripe authored Oct 18, 2022
1 parent 2e0da2d commit dc081c2
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 28 deletions.
16 changes: 10 additions & 6 deletions src/renderers/html.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import render from './html';
import { RenderableTreeNode } from '../types';
import Tag from '../tag';

function tag(
name: string,
attributes: Record<string, any> = {},
children: RenderableTreeNode[] = []
) {
return { name, attributes, children };
return new Tag(name, attributes, children);
}

describe('HTML renderer', function () {
it('rendering a tag', function () {
const example = render(tag('h1', null, ['test'])).trim();
const example = render(tag('h1', undefined, ['test'])).trim();
expect(example).toEqual('<h1>test</h1>');
});

it('rendering string child nodes', function () {
const example = tag('h1', null, ['test ', '1']);
const example = tag('h1', undefined, ['test ', '1']);
expect(render(example)).toEqual('<h1>test 1</h1>');
});

it('rendering nested tags', function () {
const example = tag('div', null, [tag('p', null, ['test'])]);
const example = tag('div', undefined, [tag('p', undefined, ['test'])]);

expect(render(example)).toEqual('<div><p>test</p></div>');
});

it('rendering parallel tags', function () {
const example = [tag('p', null, ['foo']), tag('p', null, ['bar'])];
const example = [
tag('p', undefined, ['foo']),
tag('p', undefined, ['bar']),
];

expect(render(example)).toEqual('<p>foo</p><p>bar</p>');
});

it('rendering a tag with an invalid child', function () {
const example = tag('div', null, ['test', { foo: 'bar' }]);
const example = tag('div', undefined, ['test', { foo: 'bar' }]);

expect(render(example)).toEqual('<div>test</div>');
});
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/html.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import MarkdownIt from 'markdown-it';
import Tag from '../tag';
import type { RenderableTreeNodes } from '../types';
const { escapeHtml } = MarkdownIt().utils;

Expand Down Expand Up @@ -27,7 +28,7 @@ export default function render(node: RenderableTreeNodes): string {

if (Array.isArray(node)) return node.map(render).join('');

if (node === null || typeof node !== 'object') return '';
if (node === null || typeof node !== 'object' || !Tag.isTag(node)) return '';

const { name, attributes, children = [] } = node;

Expand Down
17 changes: 5 additions & 12 deletions src/renderers/react/react.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dynamic from './react';
import renderStatic from './static';
import Tag from '../../tag';

const React = {
Fragment: 'fragment',
Expand Down Expand Up @@ -52,7 +53,7 @@ describe('React dynamic renderer', function () {

it('rendering an external component', function () {
const components = { Foo: 'bar' };
const example = { name: 'Foo', children: ['test'] };
const example = new Tag('Foo', undefined, ['test']);
const output = dynamic(example, React, { components });
expect(output).toDeepEqualSubset({
name: 'bar',
Expand All @@ -73,11 +74,7 @@ describe('React dynamic renderer', function () {
});

it('with a class attribute', function () {
const example = {
name: 'h1',
attributes: { class: 'foo bar' },
children: ['test'],
};
const example = new Tag('h1', { class: 'foo bar' }, ['test']);

const output = dynamic(example, React);
expect(output).toDeepEqualSubset({
Expand All @@ -101,11 +98,7 @@ describe('React dynamic renderer', function () {

describe('rendering built-in nodes', function () {
it('rendering a fenced code block', function () {
const example = {
name: 'pre',
attributes: { class: 'code code-ruby' },
children: ['test'],
};
const example = new Tag('pre', { class: 'code code-ruby' }, ['test']);

const output = dynamic(example, React);
expect(output).toDeepEqual({
Expand Down Expand Up @@ -164,7 +157,7 @@ describe('React static renderer', function () {

it('rendering an external component', function () {
const components = { Foo: 'bar' };
const example = { name: 'Foo', children: ['test'] };
const example = new Tag('Foo', undefined, ['test']);
const code = renderStatic(example);
const output = eval(code)({ components });

Expand Down
6 changes: 4 additions & 2 deletions src/renderers/react/react.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tagName } from './shared';
import Tag from '../../tag';
import { RenderableTreeNodes, Scalar } from '../../types';
import type { createElement, Fragment, ReactNode } from 'react';
import type { RenderableTreeNodes, Scalar } from '../../types';

type ReactShape = Readonly<{
createElement: typeof createElement;
Expand Down Expand Up @@ -30,7 +31,8 @@ export default function dynamic(
if (Array.isArray(node))
return React.createElement(React.Fragment, null, ...node.map(render));

if (node === null || typeof node !== 'object') return node;
if (node === null || typeof node !== 'object' || !Tag.isTag(node))
return node;

const {
name,
Expand Down
6 changes: 4 additions & 2 deletions src/renderers/react/static.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { tagName } from './shared';
import type { RenderableTreeNode, RenderableTreeNodes } from '../../types';
import Tag from '../../tag';
import { RenderableTreeNode, RenderableTreeNodes } from '../../types';

function renderArray(children: RenderableTreeNode[]): string {
return children.map(render).join(', ');
Expand All @@ -26,7 +27,8 @@ function render(node: RenderableTreeNodes): string {
if (Array.isArray(node))
return `React.createElement(React.Fragment, null, ${renderArray(node)})`;

if (node === null || typeof node !== 'object') return JSON.stringify(node);
if (node === null || typeof node !== 'object' || !Tag.isTag(node))
return JSON.stringify(node);

const {
name,
Expand Down
4 changes: 4 additions & 0 deletions src/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export default class Tag<
> {
readonly $$mdtype = 'Tag' as const;

static isTag = (tag: any): tag is Tag => {
return '$$mdtype' in tag && tag.$$mdtype === 'Tag';
};

name: N;
attributes: A;
children: RenderableTreeNode[];
Expand Down
13 changes: 11 additions & 2 deletions src/tags/conditional.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { isPromise } from '../utils';

import type { Node, RenderableTreeNode, Schema, Value } from '../types';
import {
MaybePromise,
Node,
RenderableTreeNode,
RenderableTreeNodes,
Schema,
Value,
} from '../types';

type Condition = { condition: Value; children: Node[] };

Expand Down Expand Up @@ -34,7 +41,9 @@ export const tagIf: Schema = {
const conditions = renderConditions(node);
for (const { condition, children } of conditions)
if (truthy(condition)) {
const nodes = children.flatMap((child) => child.transform(config));
const nodes = children.flatMap<MaybePromise<RenderableTreeNodes>>(
(child) => child.transform(config)
);
if (nodes.some(isPromise)) {
return Promise.all(nodes).then((nodes) => nodes.flat());
}
Expand Down
14 changes: 12 additions & 2 deletions src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import Tag from './tag';
import { Class } from './schema-types/class';
import { Id } from './schema-types/id';
import { isPromise } from './utils';
import type { Config, Node, NodeType, Schema, Transformer } from './types';
import type {
Config,
MaybePromise,
Node,
NodeType,
RenderableTreeNodes,
Schema,
Transformer,
} from './types';

type AttributesSchema = Schema['attributes'];

Expand Down Expand Up @@ -43,7 +51,9 @@ export default {
},

children(node: Node, config: Config = {}) {
const children = node.children.flatMap((child) => this.node(child, config));
const children = node.children.flatMap<MaybePromise<RenderableTreeNodes>>(
(child) => this.node(child, config)
);
if (children.some(isPromise)) {
return Promise.all(children);
}
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export type NodeType =

export type Primitive = null | boolean | number | string;

export type RenderableTreeNode = Tag | Primitive;
export type RenderableTreeNode = Tag | Scalar;
export type RenderableTreeNodes = RenderableTreeNode | RenderableTreeNode[];

export type Scalar = Primitive | Scalar[] | { [key: string]: Scalar };
Expand Down

0 comments on commit dc081c2

Please sign in to comment.