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

@atjson/peritext package for creating and manipulating peritext as a first class data structure #1825

Merged
merged 10 commits into from
Dec 5, 2024

Conversation

colin-alexa
Copy link
Contributor

@colin-alexa colin-alexa commented Nov 26, 2024

Adds a suite of functions for creating and manipulating peritext documents directly. The construction functions mark, block, and concat are useful for creating properly-nested documents of the sort that appear in XML-like structures. Peritext documents can of course in general have structures where marks overlap each other in arbitrary ways, but this library doesn't provide explicit means to do so.

A typical case of creating a document might look like

let doc = concat(
  block(Paragraph, { decorations: ["drop-cap"] }, "My introductory paragraph"),
  block(HorizontalRule, {}).set("selfClosing", true),
  block(Paragraph, {}, ["My", mark(Italic, {}, "fancy"), "body paragraph"])
);

PeritextBuilderStep

For convenience, several of the functions in this library (particularly block, mark, and slice) return a PeritextBuilderStep instead of just a regular peritext document. A PeritextBuilderStep is a peritext document, but when it's created it has an extra value attached to it representing the immediate product of the builder function. This value can be accessed with the getValue() method on the PeritextBuilderStep:

let paragraph: PeritextBuilderStep<Block>;

let doc = block(Div, {}, (paragraph = block(Paragraph, {}, "Hello!")));

return insertBefore(doc, paragraph.getValue().id, HorizontalRule, {});

Mutation

All of the functions that construct and modify documents have the property that they shallowly copy any peritext documents in their arguments, but directly mutate any blocks and marks on that document. This potentially-surprising behavior is intentional: this way, one can store the results of intermediate build steps and any such references to marks and blocks in the document will remain accurate after applying further build steps. documents are shallowly-copied in order for the behavior of functions like concat (which take multiple document objects and so could only mutate one of them anyway) to be consistent with the behavior of functions like groupChildren (which could in principle simply mutate the input document directly)

}
}
return copy;
} else if (typeof attribute === "string" && ids.has(attribute)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

slightly worried about unintended collisions here, but I guess that is extremely unlikely since unstable IDs are UUIDs and stable ids use a reserved construction (M00000001)

return doc;
}

function unsafe_parseRange(range: Range) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In what sense is this unsafe, that start or end might be NaN? Would it be better to throw in that case so that it might be handled in a try/catch?

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's unsafe in the sense that it doesn't make any runtime checks that the input is a valid range, so it would behave unpredictably if you pass in an arbitrary string; I don't want to add any checks though, since I expect it to be a pretty hot function in the code and I don't want to add anything that would slow it down

| Peritext
| Peritext[]
| ((block: Block) => string | Peritext | Peritext[]) = ""
): PeritextBuilderStep<Block> {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is nice!

return this.value;
}

public set<K extends keyof ReturnT>(key: K, value: ReturnT[K]) {
Copy link
Contributor

@bachbui bachbui Dec 2, 2024

Choose a reason for hiding this comment

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

somewhat surprising to me that we would want to expose this setter. What is the use case?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh nvm, was misreading. I thought this was setting the value. This allows someone to set attributes on the value block or mark. This is 👍

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, the only use case I have for it presently is setting the selfClosing property on blocks, but it would also let you set the attributes in-line if you wanted, or you could set the IDs manually. I don't think there's a like...valid reason to set the parents on a block or the range on a mark but like I don't see a reason to go out of our way to prevent that here

* @see {@link mark} - `slice(children[, attributes])` works like
* `mark(SliceAnnotation, attributes || {refs: []}, children)`
*/
export function slice(
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

}

return children;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a really nice implementation of this

Copy link
Contributor

@bachbui bachbui left a comment

Choose a reason for hiding this comment

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

Very impressive. Besides updating the README this looks great to me. AMAZING work!

Copy link
Collaborator

@tim-evans tim-evans left a comment

Choose a reason for hiding this comment

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

Could you add tests for the slice helper if that is still relevant?

@colin-alexa colin-alexa marked this pull request as ready for review December 5, 2024 20:52
@colin-alexa colin-alexa merged commit 2d6c21f into main Dec 5, 2024
3 checks passed
@colin-alexa colin-alexa deleted the peritext-builder-lib branch December 5, 2024 21:29
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.

3 participants