[ci] format

This commit is contained in:
bholmesdev 2023-07-24 23:36:32 +00:00 committed by astrobot-houston
parent 7461e82c81
commit 25e04a2ecb
13 changed files with 711 additions and 724 deletions

View file

@ -413,13 +413,11 @@ By default, Markdoc will not recognize HTML markup as semantic content.
To achieve a more Markdown-like experience, where HTML elements can be included alongside your content, set `allowHTML:true` as a `markdoc` integration option. This will enable HTML parsing in Markdoc markup.
> **Warning**
> When `allowHTML` is enabled, HTML markup inside Markdoc documents will be rendered as actual HTML elements (including `<script>`), making attack vectors like XSS possible.
>
> Ensure that any HTML markup comes from trusted sources.
```js {7} "allowHTML: true"
// astro.config.mjs
import { defineConfig } from 'astro/config';

View file

@ -11,11 +11,11 @@ import { MarkdocError, isComponentConfig, isValidUrl, prependForwardSlash } from
import { emitESMImage } from 'astro/assets/utils';
import path from 'node:path';
import type * as rollup from 'rollup';
import { htmlTokenTransform } from './html/transform/html-token-transform.js';
import type { MarkdocConfigResult } from './load-config.js';
import type { MarkdocIntegrationOptions } from './options.js';
import { setupConfig } from './runtime.js';
import { getMarkdocTokenizer } from './tokenizer.js';
import type { MarkdocIntegrationOptions } from './options.js';
import { htmlTokenTransform } from './html/transform/html-token-transform.js';
export async function getContentEntryType({
markdocConfigResult,
@ -24,8 +24,7 @@ export async function getContentEntryType({
}: {
astroConfig: AstroConfig;
markdocConfigResult?: MarkdocConfigResult;
options?: MarkdocIntegrationOptions,
options?: MarkdocIntegrationOptions;
}): Promise<ContentEntryType> {
return {
extensions: ['.mdoc'],

View file

@ -1,8 +1,9 @@
import { styleToObject } from './style-to-object.js';
import { styleToObject } from "./style-to-object.js";
export function parseInlineCSSToReactLikeObject(css: string | undefined | null): React.CSSProperties | undefined {
if (typeof css === "string") {
export function parseInlineCSSToReactLikeObject(
css: string | undefined | null
): React.CSSProperties | undefined {
if (typeof css === 'string') {
const cssObject: Record<string, string> = {};
styleToObject(css, (originalCssDirective: string, value: string) => {
const reactCssDirective = convertCssDirectiveNameToReactCamelCase(originalCssDirective);
@ -16,7 +17,7 @@ export function parseInlineCSSToReactLikeObject(css: string | undefined | null):
function convertCssDirectiveNameToReactCamelCase(original: string): string {
// capture group 1 is the character to capitalize, the hyphen is omitted by virtue of being outside the capture group
const replaced = original.replace(/-([a-z0-9])/ig, (_match, char) => {
const replaced = original.replace(/-([a-z0-9])/gi, (_match, char) => {
return char.toUpperCase();
});
return replaced;

View file

@ -117,9 +117,7 @@ export function parseInlineStyles(style, options) {
* @throws {Error}
*/
function error(msg) {
const err = new Error(
options.source + ':' + lineno + ':' + column + ': ' + msg
);
const err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg);
err.reason = msg;
err.filename = options.source;
err.line = lineno;
@ -203,7 +201,7 @@ export function parseInlineStyles(style, options) {
return pos({
type: TYPE_COMMENT,
comment: str
comment: str,
});
}
@ -230,9 +228,7 @@ export function parseInlineStyles(style, options) {
const ret = pos({
type: TYPE_DECLARATION,
property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)),
value: val
? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING))
: EMPTY_STRING
value: val ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING)) : EMPTY_STRING,
});
// ;
@ -265,7 +261,7 @@ export function parseInlineStyles(style, options) {
whitespace();
return declarations();
};
}
/**
* Trim `str`.

View file

@ -28,7 +28,7 @@
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { parseInlineStyles } from "./parse-inline-styles.js";
import { parseInlineStyles } from './parse-inline-styles.js';
/**
* Parses inline style to object.

View file

@ -1,2 +1,2 @@
export { htmlTokenTransform } from "./transform/html-token-transform";
export { htmlTag } from "./tagdefs/html.tag";
export { htmlTag } from './tagdefs/html.tag';
export { htmlTokenTransform } from './transform/html-token-transform';

View file

@ -1,19 +1,17 @@
import type { Config, Schema } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import type { Config, Schema } from '@markdoc/markdoc';
import Markdoc from '@markdoc/markdoc';
// local
import { parseInlineCSSToReactLikeObject } from "../css/parse-inline-css-to-react.js";
import { parseInlineCSSToReactLikeObject } from '../css/parse-inline-css-to-react.js';
// a Markdoc tag that will render a given HTML element and its attributes, as produced by the htmlTokenTransform function
export const htmlTag: Schema<Config, never> = {
attributes: {
name: { type: String, required: true },
attrs: { type: Object },
},
transform(node, config) {
const { name, attrs: unsafeAttributes } = node.attributes;
const children = node.transformChildren(config);
@ -21,7 +19,7 @@ export const htmlTag: Schema<Config, never> = {
const { style, ...safeAttributes } = unsafeAttributes as Record<string, unknown>;
// if the inline "style" attribute is present we need to parse the HTML into a react-like React.CSSProperties object
if (typeof style === "string") {
if (typeof style === 'string') {
const styleObject = parseInlineCSSToReactLikeObject(style);
safeAttributes.style = styleObject;
}

View file

@ -1,10 +1,8 @@
import type * as Token from 'markdown-it/lib/token';
import { Parser } from 'htmlparser2';
import { Tokenizer } from '@markdoc/markdoc';
import { Parser } from 'htmlparser2';
import type * as Token from 'markdown-it/lib/token';
export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token[] {
const output: Token[] = [];
// hold a lazy buffer of text and process it only when necessary
@ -18,21 +16,24 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
// process the current text buffer w/ Markdoc's Tokenizer for tokens
const processTextBuffer = () => {
if (textBuffer.length > 0) {
// tokenize the text buffer to look for structural markup tokens
const toks = tokenizer.tokenize(textBuffer);
// when we tokenize some raw text content, it's basically treated like Markdown, and will result in a paragraph wrapper, which we don't want
// in this scenario, we just want to generate a text token, but, we have to tokenize it in case there's other structural markup
if (toks.length === 3) {
const first = toks[0];
const second = toks[1];
const third: Token | undefined = toks.at(2);
if (first.type === 'paragraph_open' && second.type === 'inline' && (third && third.type === 'paragraph_close') && Array.isArray(second.children)) {
if (
first.type === 'paragraph_open' &&
second.type === 'inline' &&
third &&
third.type === 'paragraph_close' &&
Array.isArray(second.children)
) {
for (const tok of second.children as Token[]) {
// if the given token is a 'text' token and its trimmed content is the same as the pre-tokenized text buffer, use the original
// text buffer instead to preserve leading/trailing whitespace that is lost during tokenization of pure text content
@ -62,8 +63,8 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
};
// create an incremental HTML parser that tracks HTML tag open, close and text content
const parser = new Parser({
const parser = new Parser(
{
oncdatastart() {
inCDATA = true;
},
@ -74,7 +75,6 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
// when an HTML tag opens...
onopentag(name, attrs) {
// process any buffered text to be treated as text node before the currently opening HTML tag
processTextBuffer();
@ -90,11 +90,9 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
],
},
} as Token);
},
ontext(content: string | null | undefined) {
if (inCDATA) {
// ignore entirely while inside CDATA
return;
@ -108,7 +106,6 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
// when an HTML tag closes...
onclosetag(name) {
// process any buffered text to be treated as a text node inside the currently closing HTML tag
processTextBuffer();
@ -118,26 +115,22 @@ export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token
nesting: -1,
meta: {
tag: 'html-tag',
attributes: [
{ type: 'attribute', name: 'name', value: name },
],
attributes: [{ type: 'attribute', name: 'name', value: name }],
},
} as Token);
},
}, {
},
{
decodeEntities: false,
recognizeCDATA: true,
recognizeSelfClosing: true,
});
}
);
// for every detected token...
for (const token of tokens) {
// if it was an HTML token, write the HTML text into the HTML parser
if (token.type.startsWith('html')) {
// as the parser encounters opening/closing HTML tags, it will push Markdoc Tag nodes into the output stack
parser.write(token.content);
@ -197,14 +190,12 @@ function mutateAndCollapseExtraParagraphsUnderHtml(tokens: Token[]): void {
}
}
/**
*
* @param token
* @returns
*/
function findExtraParagraphUnderHtml(tokens: Token[]): number | null {
if (tokens.length < 5) {
return null;
}
@ -226,15 +217,15 @@ function findExtraParagraphUnderHtml(tokens: Token[]): number | null {
}
function isExtraParagraphPatternMatch(slice: Token[]): boolean {
const match = isHtmlTagOpen(slice[0])
&& isParagraphOpen(slice[1])
&& isInline(slice[2])
&& isParagraphClose(slice[3])
&& isHtmlTagClose(slice[4]);
const match =
isHtmlTagOpen(slice[0]) &&
isParagraphOpen(slice[1]) &&
isInline(slice[2]) &&
isParagraphClose(slice[3]) &&
isHtmlTagClose(slice[4]);
return match;
}
function isHtmlTagOpen(token: Token): boolean {
return token.type === 'tag_open' && token.meta && token.meta.tag === 'html-tag';
}

View file

@ -25,7 +25,9 @@ export default function markdocIntegration(options?: MarkdocIntegrationOptions):
markdocConfigResult = await loadMarkdocConfig(astroConfig);
addContentEntryType(await getContentEntryType({ markdocConfigResult, astroConfig, options }));
addContentEntryType(
await getContentEntryType({ markdocConfigResult, astroConfig, options })
);
updateConfig({
vite: {

View file

@ -10,15 +10,18 @@ import type { AstroInstance } from 'astro';
import { createComponent, renderComponent } from 'astro/runtime/server/index.js';
import type { AstroMarkdocConfig } from './config.js';
import { setupHeadingConfig } from './heading-ids.js';
import type { MarkdocIntegrationOptions } from './options.js';
import { htmlTag } from './html/tagdefs/html.tag.js';
import type { MarkdocIntegrationOptions } from './options.js';
/**
* Merge user config with default config and set up context (ex. heading ID slugger)
* Called on each file's individual transform.
* TODO: virtual module to merge configs per-build instead of per-file?
*/
export async function setupConfig(userConfig: AstroMarkdocConfig = {}, options: MarkdocIntegrationOptions | undefined): Promise<MergedConfig> {
export async function setupConfig(
userConfig: AstroMarkdocConfig = {},
options: MarkdocIntegrationOptions | undefined
): Promise<MergedConfig> {
let defaultConfig: AstroMarkdocConfig = setupHeadingConfig();
if (userConfig.extends) {
@ -41,7 +44,10 @@ export async function setupConfig(userConfig: AstroMarkdocConfig = {}, options:
}
/** Used for synchronous `getHeadings()` function */
export function setupConfigSync(userConfig: AstroMarkdocConfig = {}, options: MarkdocIntegrationOptions | undefined): MergedConfig {
export function setupConfigSync(
userConfig: AstroMarkdocConfig = {},
options: MarkdocIntegrationOptions | undefined
): MergedConfig {
const defaultConfig: AstroMarkdocConfig = setupHeadingConfig();
let merged = mergeConfig(defaultConfig, userConfig);
@ -160,7 +166,11 @@ export function collectHeadings(
}
}
export function createGetHeadings(stringifiedAst: string, userConfig: AstroMarkdocConfig, options: MarkdocIntegrationOptions | undefined) {
export function createGetHeadings(
stringifiedAst: string,
userConfig: AstroMarkdocConfig,
options: MarkdocIntegrationOptions | undefined
) {
return function getHeadings() {
/* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables).
TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself,
@ -200,6 +210,6 @@ export function createContentComponent(
// statically define a partial MarkdocConfig which registers the required "html-tag" Markdoc tag when the "allowHTML" feature is enabled
const HTML_CONFIG: AstroMarkdocConfig = {
tags: {
"html-tag": htmlTag,
'html-tag': htmlTag,
},
};

View file

@ -5,16 +5,14 @@ import type { MarkdocIntegrationOptions } from './options.js';
type TokenizerOptions = ConstructorParameters<typeof Tokenizer>[0];
export function getMarkdocTokenizer(options: MarkdocIntegrationOptions | undefined): Tokenizer {
const key = cacheKey(options);
if (!_cachedMarkdocTokenizers[key]) {
const tokenizerOptions: TokenizerOptions = {
// Strip <!-- comments --> from rendered output
// Without this, they're rendered as strings!
allowComments: true,
}
};
if (options?.allowHTML) {
// we want to allow indentation for Markdoc tags that are interleaved inside HTML block elements
@ -27,7 +25,7 @@ export function getMarkdocTokenizer(options: MarkdocIntegrationOptions | undefin
}
return _cachedMarkdocTokenizers[key];
};
}
// create this on-demand when needed since it relies on the runtime MarkdocIntegrationOptions and may change during
// the life of module in certain scenarios (unit tests, etc.)

View file

@ -9,7 +9,6 @@ async function getFixture(name) {
}
describe('Markdoc - render html', () => {
let fixture;
before(async () => {
@ -17,7 +16,6 @@ describe('Markdoc - render html', () => {
});
describe('dev', () => {
let devServer;
before(async () => {
@ -55,16 +53,13 @@ describe('Markdoc - render html', () => {
renderRandomlyCasedHTMLAttributesChecks(html);
});
});
describe('build', () => {
before(async () => {
await fixture.build();
});
it('renders content - simple', async () => {
const html = await fixture.readFile('/simple/index.html');
@ -88,7 +83,6 @@ describe('Markdoc - render html', () => {
renderRandomlyCasedHTMLAttributesChecks(html);
});
});
});
@ -115,7 +109,6 @@ function renderSimpleChecks(html) {
const p3 = document.querySelector('article > p:nth-of-type(3)');
expect(p3.children.length).to.equal(1);
expect(p3.textContent).to.equal('This is a span inside a paragraph!');
}
/** @param {string} html */
@ -164,7 +157,6 @@ function renderNestedHTMLChecks(html) {
expect(p5.id).to.equal('p5');
expect(p5.textContent).to.equal('before inner after');
expect(p5.children.length).to.equal(1);
}
/**
@ -180,17 +172,17 @@ function renderRandomlyCasedHTMLAttributesChecks(html) {
// all four <td>'s which had randomly cased variants of colspan/rowspan should all be rendered lowercased at this point
expect(td1.getAttribute("colspan")).to.equal("3");
expect(td1.getAttribute("rowspan")).to.equal("2");
expect(td1.getAttribute('colspan')).to.equal('3');
expect(td1.getAttribute('rowspan')).to.equal('2');
expect(td2.getAttribute("colspan")).to.equal("3");
expect(td2.getAttribute("rowspan")).to.equal("2");
expect(td2.getAttribute('colspan')).to.equal('3');
expect(td2.getAttribute('rowspan')).to.equal('2');
expect(td3.getAttribute("colspan")).to.equal("3");
expect(td3.getAttribute("rowspan")).to.equal("2");
expect(td3.getAttribute('colspan')).to.equal('3');
expect(td3.getAttribute('rowspan')).to.equal('2');
expect(td4.getAttribute("colspan")).to.equal("3");
expect(td4.getAttribute("rowspan")).to.equal("2");
expect(td4.getAttribute('colspan')).to.equal('3');
expect(td4.getAttribute('rowspan')).to.equal('2');
}
/**
@ -232,13 +224,15 @@ function renderComponentsHTMLChecks(html) {
expect(aside1Title.textContent.trim()).to.equal('Aside One');
const aside1Section = aside1.querySelector('section');
const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)');
expect(aside1SectionP1.textContent).to.equal('I\'m a Markdown paragraph inside an top-level aside tag');
expect(aside1SectionP1.textContent).to.equal(
"I'm a Markdown paragraph inside an top-level aside tag"
);
const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)');
expect(aside1H2_1.id).to.equal('im-an-h2-via-markdown-markup'); // automatic slug
expect(aside1H2_1.textContent).to.equal('I\'m an H2 via Markdown markup');
expect(aside1H2_1.textContent).to.equal("I'm an H2 via Markdown markup");
const aside1H2_2 = aside1Section.querySelector('h2:nth-of-type(2)');
expect(aside1H2_2.id).to.equal('h-two');
expect(aside1H2_2.textContent).to.equal('I\'m an H2 via HTML markup');
expect(aside1H2_2.textContent).to.equal("I'm an H2 via HTML markup");
const aside1SectionP2 = aside1Section.querySelector('p:nth-of-type(2)');
expect(aside1SectionP2.textContent).to.equal('Markdown bold vs HTML bold');
expect(aside1SectionP2.children.length).to.equal(2);
@ -279,7 +273,7 @@ function renderComponentsHTMLChecks(html) {
expect(section1Aside1SectionP5Span1.children.length).to.equal(1);
const section1Aside1SectionP5Span1Span1 = section1Aside1SectionP5Span1.children[0];
expect(section1Aside1SectionP5Span1Span1.textContent).to.equal(' mark');
};
}
/** @param {HTMLElement | null | undefined} el */