Skip to content

Commit

Permalink
Improve mark printing in the new editor (#1154)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed May 23, 2024
1 parent 7924ef8 commit 42975f4
Show file tree
Hide file tree
Showing 9 changed files with 607 additions and 229 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-turkeys-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystatic/core': patch
---

Improve mark printing in the new editor
15 changes: 8 additions & 7 deletions docs/keystatic.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
import { isNonEmptyArray } from 'emery/guards';

import { Flex } from '@keystar/ui/layout';
import Markdoc, { Config, Node, ValidateError, nodes } from '@markdoc/markdoc';
import Markdoc, { Config, Node, ValidateError } from '@markdoc/markdoc';
import { assert } from 'emery/assertions';

const components = {
export const components = {
tags: block({
label: 'Tags',
schema: {
Expand All @@ -24,6 +24,7 @@ const components = {
{ label: 'Existing project', value: 'Existing project' },
{ label: 'Astro', value: 'Astro' },
{ label: 'Next.js', value: 'Next.js' },
{ label: 'Remix', value: 'Remix' },
],
}),
},
Expand Down Expand Up @@ -162,16 +163,16 @@ const markdocConfig: Config = {
},
}).tags,
nodes: {
document: { ...nodes.document, render: 'Fragment' },
link: { ...nodes.link, render: 'Link' },
document: { ...Markdoc.nodes.document, render: 'Fragment' },
link: { ...Markdoc.nodes.link, render: 'Link' },
heading: {
children: ['inline'],
render: 'Heading',
attributes: {
level: { type: Number, required: true },
},
},
paragraph: { ...nodes.paragraph, render: 'Paragraph' },
paragraph: { ...Markdoc.nodes.paragraph, render: 'Paragraph' },
fence: {
render: 'CodeBlock',
attributes: {
Expand All @@ -180,8 +181,8 @@ const markdocConfig: Config = {
process: { type: Boolean, render: false, default: true },
},
},
item: { ...nodes.item, render: 'ListItem' },
hr: { ...nodes.hr, render: 'Divider' },
item: { ...Markdoc.nodes.item, render: 'ListItem' },
hr: { ...Markdoc.nodes.hr, render: 'Divider' },
code: {
render: 'Code',
attributes: {
Expand Down
17 changes: 11 additions & 6 deletions docs/src/content/pages/reader-api.mdoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
title: Reader API
summary: >-
The Reader API is a Node.js API that lets you read Keystatic content from a storage of your own choice.
The Reader API is a Node.js API that lets you read Keystatic content from a
storage of your own choice.
---
The Reader API is a Node.js API that lets you *read* Keystatic content from a storage of your own choice.
The storage can be any local directory / GitHub repository, and does not need to be the same as the one defined in the Keystatic config.
Expand All @@ -13,6 +14,7 @@ The reader API code is meant to run on the server, and not in the browser. Be su
## Usage

### Local Directory

To read from local storage, import the `createReader` function, as well as your Keystatic config file:

```javascript
Expand All @@ -23,7 +25,7 @@ import keystaticConfig from 'relative/path/to/your/keystatic.config';
You can then create a new `reader` by calling `createReader` and passing it two arguments:

1. Path to the root of your content repository
2. The Keystatic config
1. The Keystatic config

```javascript
const reader = createReader(process.cwd(), keystaticConfig);
Expand All @@ -41,10 +43,12 @@ import keystaticConfig from 'relative/path/to/your/keystatic.config';
You can then create a new `reader` by calling `createGitHubReader` and passing it the following arguments:

1. The Keystatic config
2. An options object containing:
- `repo`: The name of the content repository on GitHub (e.g. `Thinkmill/keystatic-data`)
- `token`: The Personal Access Token that allows read access to the repository. This is different from your GitHub App Client ID / Secret in `.env`.
For information on how to generate PATs, see [GitHub's documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).

1. An options object containing:

- `repo`: The name of the content repository on GitHub (e.g. `Thinkmill/keystatic-data`)
- `token`: The Personal Access Token that allows read access to the repository. This is different from your GitHub App Client ID / Secret in `.env`.
For information on how to generate PATs, see [GitHub's documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).

```javascript
const reader = createGitHubReader(keystaticConfig, {
Expand Down Expand Up @@ -125,6 +129,7 @@ If you'd rather get the `document` field data immediately, you can pass `resolve
```ts
await reader.collections.posts.read(slug, { resolvedLinkedFiles: true });
```

---

## Using TypeScript
Expand Down
200 changes: 102 additions & 98 deletions packages/keystatic/src/form/fields/markdoc/editor/markdoc/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EditorSchema, getEditorSchema } from '../schema';
import { getSrcPrefixForImageBlock } from '../images';
import { fixPath } from '../../../../../app/path-utils';
import { internalToSerialized } from '../props-serialization';
import { textblockChildren } from '../serialize-inline';

type DocumentSerializationState = {
schema: EditorSchema;
Expand All @@ -27,111 +28,114 @@ function _inline(
fragment: Fragment,
state: DocumentSerializationState
): MarkdocNode[] {
return [new Ast.Node('inline', {}, textblockChildren(fragment, state))];
return [
new Ast.Node(
'inline',
{},
textblockChildren(
fragment,
(text): MarkdocNode => new Ast.Node('text', { value: text }),
node => getLeafContent(node, state),
mark => getWrapperForMark(mark, state)
)
),
];
}

// TODO: this should handle marks spanning over multiple text nodes properly
function textblockChildren(
fragment: Fragment,
function getLeafContent(
node: ProseMirrorNode,
state: DocumentSerializationState
): MarkdocNode[] {
const children: MarkdocNode[] = [];
fragment.forEach(child => {
if (child.type === child.type.schema.nodes.hard_break) {
children.push(new Ast.Node('hardbreak'));
return;
}
if (child.type === child.type.schema.nodes.image) {
const src = child.attrs.src;

if (
typeof state.schema.config.image === 'object' &&
typeof state.schema.config.image.directory === 'string'
) {
const parent = fixPath(state.schema.config.image.directory);
if (!state.otherFiles.has(parent)) {
state.otherFiles.set(parent, new Map());
}
state.otherFiles.get(parent)!.set(child.attrs.filename, src);
} else {
state.extraFiles.set(child.attrs.filename, src);
}
): MarkdocNode | undefined {
const { schema } = state;
if (node.type === schema.nodes.hard_break) {
return new Ast.Node('hardbreak');
}
if (node.type === schema.nodes.image) {
const { src, filename } = node.attrs;

children.push(
new Ast.Node('image', {
src: encodeURI(
`${getSrcPrefixForImageBlock(state.schema.config, state.slug)}${
child.attrs.filename
}`
),
alt: child.attrs.alt,
title: child.attrs.title,
})
);
}
const componentConfig = state.schema.components[child.type.name];
if (componentConfig?.kind === 'inline') {
const tag = new Ast.Node(
'tag',
internalToSerialized(componentConfig.schema, child.attrs.props, state),
[],
child.type.name
);
tag.inline = true;
children.push(tag);
return;
}
if (child.text !== undefined) {
const textNode = new Ast.Node('text', { content: child.text }, []);
let node = textNode;
const schema = getEditorSchema(child.type.schema);
let linkMark: Mark | undefined;
for (const mark of child.marks) {
if (mark.type === schema.marks.link) {
linkMark = mark;
continue;
}
const componentConfig = schema.components[mark.type.name];
if (componentConfig) {
node = new Ast.Node(
'tag',
internalToSerialized(
componentConfig.schema,
mark.attrs.props,
state
),
[node],
mark.type.name
);
node.inline = true;
continue;
}
let type: NodeType | undefined;
if (mark.type === schema.marks.bold) {
type = 'strong';
}
if (mark.type === schema.marks.code) {
textNode.type = 'code';
continue;
}
if (mark.type === schema.marks.italic) {
type = 'em';
}
if (mark.type === schema.marks.strikethrough) {
type = 's';
}
if (type) {
node = new Ast.Node(type, { type: mark.type.name }, [node]);
}
}
if (linkMark) {
node = new Ast.Node('link', { href: linkMark.attrs.href }, [node]);
if (
typeof state.schema.config.image === 'object' &&
typeof state.schema.config.image.directory === 'string'
) {
const parent = fixPath(state.schema.config.image.directory);
if (!state.otherFiles.has(parent)) {
state.otherFiles.set(parent, new Map());
}
children.push(node);
state.otherFiles.get(parent)!.set(filename, src);
} else {
state.extraFiles.set(filename, src);
}
});

return children;
return new Ast.Node('image', {
src: encodeURI(
`${getSrcPrefixForImageBlock(state.schema.config, state.slug)}${
node.attrs.filename
}`
),
alt: node.attrs.alt,
title: node.attrs.title,
});
}
const componentConfig = state.schema.components[node.type.name];
if (componentConfig?.kind === 'inline') {
const tag = new Ast.Node(
'tag',
internalToSerialized(componentConfig.schema, node.attrs.props, state),
[],
node.type.name
);
tag.inline = true;
return tag;
}
if (node.text !== undefined) {
return new Ast.Node(
node.marks.some(x => x.type === schema.marks.code) ? 'code' : 'text',
{ content: node.text }
);
}
}

function getWrapperForMark(
mark: Mark,
state: DocumentSerializationState
): MarkdocNode | undefined {
const { schema } = state;

if (mark.type === schema.marks.code) {
return;
}
const componentConfig = schema.components[mark.type.name];
if (componentConfig) {
const node = new Ast.Node(
'tag',
internalToSerialized(componentConfig.schema, mark.attrs.props, state),
[],
mark.type.name
);
node.inline = true;
return node;
}

let type: NodeType | undefined;
if (mark.type === schema.marks.bold) {
type = 'strong';
}
if (mark.type === schema.marks.italic) {
type = 'em';
}
if (mark.type === schema.marks.strikethrough) {
type = 's';
}

if (type) {
return new Ast.Node(type, {}, []);
}
if (mark.type === schema.marks.link) {
return new Ast.Node('link', {
href: mark.attrs.href,
title: mark.attrs.title,
});
}
}

export function proseMirrorToMarkdoc(
Expand Down
Loading

0 comments on commit 42975f4

Please sign in to comment.