Skip to content

Commit

Permalink
feat: move autoUnwrap & CodeBlock props out of experimental
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz committed Nov 19, 2024
1 parent 28dcb7b commit 1699b7a
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 61 deletions.
44 changes: 30 additions & 14 deletions src/from-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { parseEntities } from 'parse-entities'
import { kebabCase } from 'scule'
import type { Token, CompileContext, Container, Nodes } from './micromark-extension/types'
import type { RemarkMDCOptions } from './types'
import { NON_UNWRAPPABLE_TYPES } from './utils'
import { CONTAINER_NODE_TYPES, NON_UNWRAPPABLE_TYPES } from './utils'

export default (opts: RemarkMDCOptions = {}) => {
const canContainEols = ['textComponent']

const experimentalCodeBlockYamlProps = (node: Container) => {
const applyYamlCodeBlockProps = (node: Container) => {
const firstSection = node.children[0] as Container
if (
firstSection &&
Expand All @@ -27,23 +27,39 @@ export default (opts: RemarkMDCOptions = {}) => {
firstSection.children!.splice(0, 1)
}
}
const experimentalAutoUnwrap = (node: Container) => {
if (opts.experimental?.autoUnwrap && NON_UNWRAPPABLE_TYPES.includes(node.type)) {
const nonSlotChildren = (node.children).filter((child: any) => child.type !== 'componentContainerSection')
if (nonSlotChildren.length === 1 && !NON_UNWRAPPABLE_TYPES.includes(nonSlotChildren[0].type)) {
const nonSlotChildIndex = node.children.indexOf(nonSlotChildren[0])

node.children.splice(nonSlotChildIndex, 1, ...((nonSlotChildren[0] as Container)?.children || []))
node.mdc = node.mdc || {}
node.mdc.unwrapped = nonSlotChildren[0].type
}
const applyAutomaticUnwrap = (node: Container, { safeTypes = [] }: Exclude<RemarkMDCOptions['autoUnwrap'], boolean | undefined>) => {
if (!CONTAINER_NODE_TYPES.has(node.type)) {
// unwrap only applicable for container components
return
}

const nonSlotChildren = (node.children).filter((child: any) => child.type !== 'componentContainerSection')
if (nonSlotChildren.length !== 1) {
// unwrapp only works when container has only one child (slots are separated children)
return
}

const child = nonSlotChildren[0]
if (NON_UNWRAPPABLE_TYPES.has(child.type) || safeTypes.includes(child.type)) {
// Ignore child if it's in safe types list
return
}

const childIndex = node.children.indexOf(child)

node.children.splice(childIndex, 1, ...((child as Container)?.children || []))
node.mdc = node.mdc || {}
node.mdc.unwrapped = child.type
}

const processNode = (node: Container) => {
if (opts.experimental?.componentCodeBlockYamlProps) {
experimentalCodeBlockYamlProps(node)
if (opts.yamlCodeBlockProps) {
applyYamlCodeBlockProps(node)
}
if (opts.autoUnwrap) {
applyAutomaticUnwrap(node, typeof opts.autoUnwrap === 'boolean' ? {} : opts.autoUnwrap)
}
experimentalAutoUnwrap(node)
}
const enter = {
componentContainer: enterContainer,
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ interface ComponentNode extends Node {
export default <Plugin<Array<RemarkMDCOptions>>> function remarkMDC (opts: RemarkMDCOptions = {}) {
const data: Record<string, any> = this.data()

/**
* Convert experimental options
* Will drop experimental options in v5
*/
if (opts.autoUnwrap === undefined && opts.experimental?.autoUnwrap) {
opts.autoUnwrap = opts.experimental.autoUnwrap ? { safeTypes: [] } : false
}
if (opts.yamlCodeBlockProps === undefined && opts.experimental?.componentCodeBlockYamlProps) {
opts.yamlCodeBlockProps = opts.experimental.componentCodeBlockYamlProps
}

add('micromarkExtensions', syntax())
add('fromMarkdownExtensions', fromMarkdown(opts))
add('toMarkdownExtensions', toMarkdown(opts))
Expand Down
109 changes: 65 additions & 44 deletions src/to-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { RemarkMDCOptions } from './types'
import { NON_UNWRAPPABLE_TYPES } from './utils'
import { type Container } from './micromark-extension/types'

type NodeContainerComponent = Parents & { name: string; fmAttributes?: Record<string, any> }

const own = {}.hasOwnProperty

const shortcut = /^[^\t\n\r "#'.<=>`}]+$/
Expand Down Expand Up @@ -39,21 +41,54 @@ function compilePattern (pattern: Unsafe) {
type NodeComponentContainerSection = Parents & { name: string }

export default (opts: RemarkMDCOptions = {}) => {
const experimentalAutoUnwrap = (node: Container) => {
if (opts?.experimental?.autoUnwrap) {
if (node.mdc?.unwrapped) {
node.children = [
{
type: node.mdc.unwrapped as any,
children: node.children.filter((child: RootContent) => !NON_UNWRAPPABLE_TYPES.includes(child.type))
},
...node.children.filter((child: RootContent) => NON_UNWRAPPABLE_TYPES.includes(child.type))
]
}
const applyAutomaticUnwrap = (node: Container, { safeTypes = [] }: Exclude<RemarkMDCOptions['autoUnwrap'], boolean | undefined>) => {
const isSafe = (type: string) => NON_UNWRAPPABLE_TYPES.has(type) || safeTypes.includes(type)
if (!node.mdc?.unwrapped) {
return
}
node.children = [
{
type: node.mdc.unwrapped as any,
children: node.children.filter((child: RootContent) => !isSafe(child.type))
},
...node.children.filter((child: RootContent) => isSafe(child.type))
]
}

const frontmatter = (node: NodeContainerComponent) => {
const entries = Object.entries(node.fmAttributes || {})

if (entries.length === 0) {
return ''
}

const attrs = entries
.sort(([key1], [key2]) => key1.localeCompare(key2))
.reduce((acc, [key, value2]) => {
// Parse only JSON objects. `{":key:": "value"}` can be used for binding data to frontmatter.
if (key?.startsWith(':') && isValidJSON(value2)) {
try {
value2 = JSON.parse(value2)
} catch {
// ignore
}
key = key.slice(1)
}
acc[key] = value2
return acc
}, {} as Record<string, any>)

return '\n' + (
opts?.yamlCodeBlockProps
? stringifyCodeBlockProps(attrs).trim()
: stringifyFrontMatter(attrs).trim()
)
}

const processNode = (node: Container) => {
experimentalAutoUnwrap(node)
if (opts.autoUnwrap) {
applyAutomaticUnwrap(node, typeof opts.autoUnwrap === 'boolean' ? {} : opts.autoUnwrap)
}
}

function componentContainerSection (node: NodeComponentContainerSection, _: any, context: any) {
Expand Down Expand Up @@ -88,7 +123,6 @@ export default (opts: RemarkMDCOptions = {}) => {
return value
}

type NodeContainerComponent = Parents & { name: string; fmAttributes?: Record<string, any> }
let nest = 0
function containerComponent (node: NodeContainerComponent, _: any, context: any) {
context.indexStack = context.stack
Expand All @@ -97,37 +131,6 @@ export default (opts: RemarkMDCOptions = {}) => {
const exit = context.enter(node.type)
let value = prefix + (node.name || '') + label(node, context)

const attributesText = attributes(node, context)
const fmAttributes: Record<string, string> = node.fmAttributes || {}

if ((value + attributesText).length > (opts?.maxAttributesLength || 80) || Object.keys(fmAttributes).length > 0 || node.children?.some((child: RootContent) => child.type === 'componentContainerSection')) {
Object.assign(fmAttributes, (node as any).attributes)
} else {
value += attributesText
}
let subvalue

// Convert attributes to YAML FrontMatter format
if (Object.keys(fmAttributes).length > 0) {
const attrs = Object.entries(fmAttributes)
.sort(([key1], [key2]) => key1.localeCompare(key2))
.reduce((acc, [key, value2]) => {
// Parse only JSON objects. `{":key:": "value"}` can be used for binding data to frontmatter.
if (key?.startsWith(':') && isValidJSON(value2)) {
try {
value2 = JSON.parse(value2)
} catch {
// ignore
}
key = key.slice(1)
}
acc[key] = value2
return acc
}, {} as Record<string, any>)
const fm = opts?.experimental?.componentCodeBlockYamlProps ? stringifyCodeBlockProps(attrs) : stringifyFrontMatter(attrs)
value += '\n' + fm.trim()
}

// Move default slot's children to the beginning of the content
const defaultSlotChildren = node.children.filter((child: any) => child.type !== 'componentContainerSection')
const slots = node.children.filter((child: any) => child.type === 'componentContainerSection')
Expand All @@ -137,8 +140,26 @@ export default (opts: RemarkMDCOptions = {}) => {
...slots
]

// ensure fmAttributes exists
node.fmAttributes = node.fmAttributes || {}
const attributesText = attributes(node, context)
if (
(value + attributesText).length > (opts?.maxAttributesLength || 80) ||
Object.keys(node.fmAttributes).length > 0 || // remove: allow using both yaml and inline attributes simentensoly
node.children?.some((child: RootContent) => child.type === 'componentContainerSection') // remove: allow using both yaml and inline attributes simentensoly
) {
// add attributes to frontmatter
Object.assign(node.fmAttributes, (node as any).attributes)
// clear attributes
;(node as any).attributes = []
}

processNode(node as any)

value += attributes(node, context)
value += frontmatter(node)

let subvalue
if ((node.type as string) === 'containerComponent') {
subvalue = content(node, context)
if (subvalue) { value += '\n' + subvalue }
Expand Down
10 changes: 10 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ interface ComponentHandler {
export interface RemarkMDCOptions {
components?: ComponentHandler[]
maxAttributesLength?: number
autoUnwrap?: boolean | {
safeTypes?: Array<string>
}
yamlCodeBlockProps?: boolean
experimental?: {
/**
* @deprecated This feature is out of experimental, use `autoUnwrap`
*/
autoUnwrap?: boolean
/**
* @deprecated This feature is out of experimental, use `yamlCodeBlockProps`
*/
componentCodeBlockYamlProps?: boolean
}
}
13 changes: 10 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
export const NON_UNWRAPPABLE_TYPES = [
export const CONTAINER_NODE_TYPES = new Set([
'componentContainerSection',
'containerComponent',
'leafComponent'
])

export const NON_UNWRAPPABLE_TYPES = new Set([
'componentContainerSection',
'componentContainerDataSection',
'containerComponent',
'leafComponent',
'table',
'pre',
'code'
]
'code',
'textComponent'
])

0 comments on commit 1699b7a

Please sign in to comment.