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

fix: updates to html block #906

Merged
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 3 additions & 1 deletion components/HTMLBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ const extractScripts = (html: string = ''): [string, () => void] => {
return [cleaned, () => scripts.map(js => window.eval(js))];
};

const HTMLBlock = ({ children = '', runScripts = false, safeMode = false }) => {
const HTMLBlock = ({ children = '', runScripts, safeMode = false }) => {
let html = children;
runScripts = typeof runScripts !== 'boolean' ? (runScripts === 'true' ? true : false) : runScripts;
jennspencer marked this conversation as resolved.
Show resolved Hide resolved

if (typeof html !== 'string') html = renderToStaticMarkup(html);
const [cleanedHtml, exec] = extractScripts(html);

Expand Down
12 changes: 4 additions & 8 deletions docs/html-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,14 @@ title: 'HTML Blocks'

## HTML in template literal

<HTMLBlock>
{`
<HTMLBlock>{`
<script>console.log(true, 3);</script>
<h3>Header 3</h3>
<p>Paragraph with <em>italics</em> and <strong>bold stuff</strong>.</p>
<div style="padding-left: 20px; color: blue">
Behold, I am blue and indented!
</div>
`}
</HTMLBlock>
`}</HTMLBlock>


## JSX in safe mode
Expand All @@ -59,13 +57,11 @@ title: 'HTML Blocks'

## HTML in safe mode

<HTMLBlock safeMode={true}>
{`
<HTMLBlock safeMode={true}>{`
<script>console.log(true, 5);</script>
<h3>Header 5</h3>
<p>Paragraph with <em>italics</em> and <strong>bold stuff</strong>.</p>
<div style="padding-left: 20px; color: blue">
Behold, I am blue and indented!
</div>
`}
</HTMLBlock>
`}</HTMLBlock>
2 changes: 1 addition & 1 deletion lib/run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const makeUseMDXComponents = (more: ReturnType<UseMdxComponents> = {}): UseMdxCo
img: Components.Image,
table: Components.Table,
'code-tabs': Components.CodeTabs,
'html-block': Components.HTMLBlock,
'embed-block': Components.Embed,
'html-block': Components.HTMLBlock,
'image-block': Components.Image,
'table-of-contents': Components.TableOfContents,
// @ts-expect-error
Expand Down
9 changes: 6 additions & 3 deletions processor/compile/html-block.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { HTMLBlock } from '../../types';
import { reformatHTML, getHProps } from '../utils'

const htmlBlock = (node: HTMLBlock) => {
const html = node.data.hProperties.html;
const attributes = Object.keys(node.data?.hProperties).map(key => `${key}="${node.data?.hProperties[key]}"`).join(' ')
return `<HTMLBlock${attributes && ' ' + attributes}>{\`${ html }\`}</HTMLBlock>`;
const { runScripts, html } = getHProps<HTMLBlock['data']['hProperties']>(node);

return `<HTMLBlock${runScripts != null ? ` runScripts="${runScripts}"` : ''}>{\`
${ reformatHTML(html) }
\`}</HTMLBlock>`;
}

export default htmlBlock;
26 changes: 23 additions & 3 deletions processor/transform/readme-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { BlockContent, Code, Parents, Table } from 'mdast';
import { Transform } from 'mdast-util-from-markdown';

import { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
import { Callout, EmbedBlock, ImageBlock } from 'types';
import { Callout, EmbedBlock, HTMLBlock, ImageBlock } from 'types';
import { visit } from 'unist-util-visit';

import { getAttrs, isMDXElement } from '../utils';
import { getAttrs, isMDXElement, getChildren, formatHTML } from '../utils';

const types = {
Callout: NodeTypes['callout'],
Expand All @@ -15,6 +15,7 @@ const types = {
EmbedBlock: NodeTypes['embed-block'],
Glossary: NodeTypes['glossary'],
ImageBlock: NodeTypes['image-block'],
HTMLBlock: NodeTypes.htmlBlock,
Table: 'table',
Variable: NodeTypes['variable'],
td: 'tableCell',
Expand Down Expand Up @@ -49,7 +50,6 @@ const coerceJsxToMd =
parent.children[index] = mdNode;
} else if (node.name === 'Image') {
const { position } = node;

const { alt = '', url, title = null } = getAttrs<Pick<ImageBlock, 'alt' | 'title' | 'url'>>(node);
const attrs = getAttrs<ImageBlock['data']['hProperties']>(node);
const mdNode: ImageBlock = {
Expand All @@ -64,6 +64,26 @@ const coerceJsxToMd =
},
};

parent.children[index] = mdNode;
} else if (node.name === 'HTMLBlock') {
const { position } = node;
const children = getChildren<HTMLBlock['children']>(node);
kellyjosephprice marked this conversation as resolved.
Show resolved Hide resolved
const { runScripts } = getAttrs<Pick<HTMLBlock['data']['hProperties'], 'runScripts'>>(node);
const html = formatHTML(children.map(({ value }) => value).join(''));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think trimming the content is fine, but I worry that changing the indent could be problematic. I'm inclined to say we shouldn't munge the string beyond trimming leading and trailing whitespce?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i'm not a big fan of the munging, which is why i tried to keep it to a minimum. if you just .trim(), the raw HTML displays in a messy way in the widget code editor/the recompiled markdown :(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it reindents the HTML so it's not at the same indentation as the main HTMLBlock component. i admit it was a subjective style choice, but it helps make the md doc easier to read?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(while also keeping the original structure as much as possible)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and i totally admit that there are a few scenarios here i need to account for)


const mdNode: HTMLBlock = {
position,
children: [{ type: 'text', value: html }],
type: NodeTypes.htmlBlock,
data: {
hName: 'html-block',
hProperties: {
...(runScripts && { runScripts }),
html,
},
},
};

parent.children[index] = mdNode;
} else if (node.name === 'Table') {
const { children, position } = node;
Expand Down
122 changes: 112 additions & 10 deletions processor/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
import { Node } from 'mdast';
import { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
import { MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression } from 'mdast-util-mdx';

export const formatHProps = <T>(node: Node) => {
const hProps = getHProps(node);
const hPropKeys = getHPropKeys(node) as string[];
return hPropKeys.map(key => `${key}="${hProps[key]}"`).join(' ') as T;
/**
* Formats the hProperties of a node as a string, so they can be compiled back into JSX/MDX.
* This currently sets all the values to a string since we process/compile the MDX on the fly
* through the editor, and it'll throw errors over malformed JSX. TODO: fix this.
*
* @template T
* @param {Node} node
* @returns {string} formatted hProperties as JSX attributes
*/
export const formatHProps = <T>(node: Node): string => {
const hProps = getHProps<T>(node);
const hPropKeys = getHPropKeys<T>(node) as string[];
return hPropKeys.map(key => `${key}="${hProps[key]}"`).join(' ');
}

export const getHProps = <T>(node: Node) => {
/**
* Returns the hProperties of a node.
*
* @template T
* @param {Node} node
* @returns {T} hProperties
*/
export const getHProps = <T>(node: Node): T => {
const hProps = node.data?.hProperties || {};
return hProps as T;
}

export const getHPropKeys = <T>(node: Node) => {
const hProps = getHProps(node);
return Object.keys(hProps) || [] as T;
/**
* Returns array of hProperty keys.
*
* @template T
* @param {Node} node
* @returns {Array} array of hProperty keys
*/
export const getHPropKeys = <T>(node: Node): any => {
const hProps = getHProps<T>(node);
return Object.keys(hProps) || [];
}

export const getAttrs = <T>(jsx: MdxJsxFlowElement | MdxJsxTextElement) =>
/**
* Gets the attributes of an MDX element and returns them as an object of hProperties.
*
* @template T
* @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
* @returns {T} object of hProperties
*/
export const getAttrs = <T>(jsx: MdxJsxFlowElement | MdxJsxTextElement): any =>
jsx.attributes.reduce((memo, attr) => {
if ('name' in attr) {
memo[attr.name] = attr.value;
Expand All @@ -26,6 +56,78 @@ export const getAttrs = <T>(jsx: MdxJsxFlowElement | MdxJsxTextElement) =>
return memo;
}, {} as T);

/**
* Gets the children of an MDX element and returns them as an array of Text nodes.
* Currently only being used by the HTML Block component, which only expects a single text node.
*
* @template T
* @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
* @returns {Array} array of child text nodes
*/
export const getChildren = <T>(jsx: MdxJsxFlowElement | MdxJsxTextElement): any =>
jsx.children.reduce((memo, child: MdxFlowExpression, i) => {
memo[i] = {
type: 'text',
value: child.value,
position: child.position,
};
return memo;
}, [] as T);

/**
* Tests if a node is an MDX element.
* TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say
*
* @param {Node} node
* @returns {(node is MdxJsxFlowElement | MdxJsxTextElement)}
*/
export const isMDXElement = (node: Node): node is MdxJsxFlowElement | MdxJsxTextElement => {
return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type);
}

/**
* Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
* and unindents the HTML.
*
* @param {string} html
* @returns {string} formatted HTML
*/
export const formatHTML = (html: string): string => {
// Remove leading/trailing backticks if present, since they're used to keep the HTML
// from being parsed prematurely
if (html.startsWith('`') && html.endsWith('`')) {
kellyjosephprice marked this conversation as resolved.
Show resolved Hide resolved
html = html.slice(1, -1);
}
// Removes the leading/trailing newlinesl
const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');

// Get the number of spaces in the first line to determine the tab size
const tab = cleaned.match(/^\s*/)[0].length;

// Remove the first indentation level from each line
const tabRegex = new RegExp(`^\\s{${tab}}`, 'gm');
const unindented = cleaned.replace(tabRegex, '');

return unindented;
}

/**
* Reformat HTML for the markdown/mdx by adding an indentation to each line. This assures that the
* HTML is indentend properly within the HTMLBlock component when rendered in the markdown/mdx.
*
* @param {string} html
* @param {number} [indent=2]
* @returns {string} re-formatted HTML
*/
export const reformatHTML = (html: string, indent: number = 2): string => {
// Remove leading/trailing newlines
const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');

// Create a tab/indent with the specified number of spaces
const tab = ' '.repeat(indent);

// Indent each line of the HTML (converts to an array, indents each line, then joins back)
const indented = cleaned.split('\n').map((line: string) => `${tab}${line}`).join('\n');

return indented;
}
4 changes: 3 additions & 1 deletion types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Code, Data, Literal, Parent, Blockquote, Node, Root } from 'mdast';
import { Code, Data, Literal, Parent, Blockquote, Node, Root, Text } from 'mdast';

import { NodeTypes } from './enums';
import { Element } from 'hast';
Expand Down Expand Up @@ -46,9 +46,11 @@ interface EmbedBlock extends Node {

interface HTMLBlock extends Node {
type: NodeTypes.htmlBlock;
children: Text[];
data: Data & {
hName: 'html-block';
hProperties: {
runScripts?: boolean | string;
html: string;
jennspencer marked this conversation as resolved.
Show resolved Hide resolved
};
};
Expand Down
Loading