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

Introduces slots #356

Merged
merged 2 commits into from Apr 17, 2023
Merged

Introduces slots #356

merged 2 commits into from Apr 17, 2023

Conversation

rpaul-stripe
Copy link
Contributor

@rpaul-stripe rpaul-stripe commented Apr 17, 2023

This PR implements the slots proposal in Markdoc. Slots make it possible for a custom tag to accept multiple sets of children, providing an to attributes that is suitable for cases where you want to accept arbitrary Markdoc content instead of a conventional attribute type.

  • When slots are enabled, the parser automatically takes content that is wrapped inside of a slot tag and adds it to the Node object's slots property instead of the Node's children.
  • The AST Node class stores slots in a slots property on the Node object
    • The Node.walk iterator function recursively yields the slots and their children before yielding the Node's children
  • The top-level parse function now accepts an object with parse settings, which takes the shape {file: string, slots: boolean}
    • Special-case handling for slots during parsing only occurs when the value of the slots configuration property is true, otherwise each slot in a document is treated like a conventional tag named slot.
    • For backwards compatibility, the settings parameter for the parse function can also be a string by itself, which is treated as {file: string}.
    • Test cases to validate settings behavior are added in parser.test.ts
  • The Marktest testing tool automatically enables slots when the test case has slots: true in the top-level test object
    • Adds a bunch of Marktest cases to test verify that the slots functionality is working as expected
  • Schema types were tweaked to support declaring slots inside of tag and node definitions
    • A slot is declared a bit like an attribute, the user can specify whether it is required and whether it renders
    • The validator explicitly checks to make sure that required slots are fulfilled and displays a new slot-missing-required error when one is missing
    • The validator explicitly checks to make sure that there are no extra slots added to content that are not supported in the schema and shows a slot-undefined error when one is detected
  • A built-in slot tag is defined in the schema
    • The slot tag must have a primary attribute with a String type value that reflects the slot's name
  • The trasnsformer.attributes function was updated to process slots on a given Node instance after processing attributes. It iterates over slots that are declared in the schema for the Node instance, renders each slot node, and adds them to the rendered attribute object

Copy link
Contributor

@mfix-stripe mfix-stripe left a comment

Choose a reason for hiding this comment

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

Wouldn't call it a full review, but took a quick pass and left some comments @rpaul-stripe.

src/ast/node.ts Outdated
}

*walk(): Generator<Node, void, unknown> {
for (const slot of Object.values(this.slots)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be faster to make this.slots default to undefined and avoid the Object.values(this.slots) in the default case?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess alternatively you could simplify this function by doing:

    for (const child of this.children.concat(Object.values(this.slots))) {
      ...

or something similar

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm changing it to: for (const child of [...Object.values(this.slots), ...this.children]) {. I don't think it materially impacts performance either way.

tag === 'slot' &&
typeof node.attributes.primary === 'string'
)
parent.slots[node.attributes.primary] = node;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make children a reserved slot key? Or is there no reason to avoid this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is necessary. In the event that someone uses children as the name of the slot, the value will appear as expected in the render tree but will be replaced by the actual children in the React props.

This is already the behavior we have today with attributes and we don't enforce any restrictions to prevent that. React itself does pretty much the same thing if use a "children" attribute in JSX as well as passing in actual children.

errors.push({
id: 'slot-missing-required',
level: 'error',
message: `Missing required slot: '${key}'`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a unit test for this string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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


export type ParserArgs = {
file?: string;
slots?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm usually a little wary of adding boolean config options like this. WDYT about using an enum instead? Like slotMode: 'enabled' or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer the boolean for simplicity and because it's consistent with how tokenizer options work.


export default {
if: tagIf,
else: tagElse,
partial,
table,
slot,
Copy link
Contributor

Choose a reason for hiding this comment

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

alphabetize? 🙂

src/parser.ts Show resolved Hide resolved
src/parser.ts Show resolved Hide resolved
@rpaul-stripe
Copy link
Contributor Author

@mfix thank you for feedback! I tweaked the Node.walk function a little as suggested and I alphabetized the properties in the tags export. I also realized that there's no test case for the behavior of Node.walk with slots, so I added one.

Copy link
Contributor

@mfix-stripe mfix-stripe left a comment

Choose a reason for hiding this comment

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

LGTM

@rpaul-stripe rpaul-stripe merged commit ce14a80 into main Apr 17, 2023
2 checks passed
@rpaul-stripe rpaul-stripe deleted the slots branch April 17, 2023 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants