Skip to content

Commit 8b224d7

Browse files
committed
refactor code around conversion to prosemirror to make it easier for me to understand
1 parent dd3098b commit 8b224d7

File tree

6 files changed

+213
-230
lines changed

6 files changed

+213
-230
lines changed

packages/editor/src/api/pandoc.ts

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { kLinkChildren } from './link';
2424

2525
import { BibliographyResult, PandocServer, PandocApiVersion, PandocAst, PandocToken } from 'editor-types';
2626
import { PandocExtensions } from './pandoc-types';
27-
export type { BibliographyResult, PandocServer,PandocApiVersion, PandocAst, PandocToken, PandocExtensions };
27+
export type { BibliographyResult, PandocServer, PandocApiVersion, PandocAst, PandocToken, PandocExtensions };
2828

2929

3030
export function imageAttributesAvailable(pandocExtensions: PandocExtensions) {
@@ -257,52 +257,73 @@ export function stringifyTokens(c: PandocToken[], unemoji = false): string {
257257
}
258258

259259
export function forEachToken(tokens: PandocToken[], f: (tok: PandocToken) => void) {
260-
mapTokens(tokens, (tok: PandocToken) => {
260+
mapTokensRecursive(tokens, (tok: PandocToken) => {
261261
f(tok);
262262
return tok;
263263
});
264264
}
265265

266-
export function mapTokens(tokens: PandocToken[], f: (tok: PandocToken) => PandocToken) {
267-
function isToken(val: unknown) {
268-
if (val !== null && typeof val === 'object') {
269-
return Object.prototype.hasOwnProperty.call(val, 't');
270-
} else {
271-
return false;
272-
}
273-
}
266+
const isObject = (val: unknown): val is object =>
267+
val !== null &&
268+
typeof val === 'object';
274269

275-
function tokenHasChildren(tok: PandocToken) {
276-
return tok !== null && typeof tok === 'object' && Array.isArray(tok.c);
277-
}
270+
const isToken = (val: unknown): val is PandocToken =>
271+
isObject(val) &&
272+
val.hasOwnProperty('t');
278273

279-
function mapValue(val: unknown): unknown {
280-
if (isToken(val)) {
281-
return mapToken(val as PandocToken);
282-
} else if (Array.isArray(val)) {
283-
return val.map(mapValue);
284-
} else {
285-
return val;
286-
}
287-
}
274+
/**
275+
* @param f A function to be enhanced.
276+
* @returns An enhanced function. If the input to the enhanced function is a
277+
* non-array value, then `f` is simply applied to it.
278+
* If the input to the enhanced function is an array, optionally of recursively nested arrays,
279+
* then `f` is applied to all non-array values in the array and all recursively nested arrays.
280+
*/
281+
const mapRecursiveArray = (f: (v: unknown) => any) => {
282+
283+
const mappedF = (val: unknown): any =>
284+
Array.isArray(val) ?
285+
val.map(mappedF) : f(val);
286+
287+
return mappedF;
288+
};
288289

289-
function mapToken(tok: PandocToken): PandocToken {
290+
/**
291+
* @param f A function from PandocToken to PandocToken to be enhanced.
292+
* @returns An enhanced function that applies `f` to an input token, and
293+
* then applies itself to any tokens in the output's content i.e. `mappedTok.c`,
294+
* even if token is inside nested arrays in the output's content.
295+
*/
296+
const mapTokenAndContentTokensRecursively = (f: (tok: PandocToken) => PandocToken) => {
297+
const mappedF = (tok: PandocToken) => {
290298
const mappedTok = f(tok);
291-
if (tokenHasChildren(mappedTok)) {
292-
mappedTok.c = mappedTok.c.map(mapValue);
293-
}
299+
300+
const recursiveMappedF = mapRecursiveArray((v) => isToken(v) ? mappedF(v) : v);
301+
mappedTok.c = recursiveMappedF(mappedTok.c);
302+
294303
return mappedTok;
295-
}
304+
};
296305

297-
return tokens.map(mapToken);
306+
return mappedF;
307+
};
308+
309+
/**
310+
*
311+
* @param tokens An array of `PandocToken`s
312+
* @param f A function to be applied to each token that returns a new or modified token
313+
* @returns The array of tokens resulting from applying `f` to each token in `token`s, and
314+
* also recursively applying `f` to any tokens in the resulting token's content (even if the content
315+
* consists of recursively nested arrays, and the tokens are in the nested arrays).
316+
*/
317+
export function mapTokensRecursive(tokens: PandocToken[], f: (tok: PandocToken) => PandocToken) {
318+
return tokens.map(mapTokenAndContentTokensRecursively(f));
298319
}
299320

300321
export function tokenTextEscaped(t: PandocToken) {
301322
return t.c.replace(/\\/g, `\\\\`);
302323
}
303324

304325
// sort marks by priority (in descending order)
305-
export function marksByPriority(marks: readonly Mark[], markWriters: { [key: string]: PandocMarkWriter }) {
326+
export function marksByPriority(marks: readonly Mark[], markWriters: { [key: string]: PandocMarkWriter; }) {
306327
return Array.prototype.sort.call(marks, (a: Mark, b: Mark) => {
307328
const aPriority = markWriters[a.type.name].priority;
308329
const bPriority = markWriters[b.type.name].priority;

packages/editor/src/api/pandoc_capsule.ts

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Schema } from 'prosemirror-model';
1717

1818
import { base64Encode, base64Decode } from './base64';
1919

20-
import { PandocToken, ProsemirrorWriter, mapTokens, PandocTokenType } from './pandoc';
20+
import { PandocToken, ProsemirrorWriter, mapTokensRecursive, PandocTokenType } from './pandoc';
2121

2222
// constants used for creating/consuming capsules
2323
const kFieldDelimiter = '\n';
@@ -65,7 +65,7 @@ export interface PandocBlockCapsuleFilter {
6565
p2: string,
6666
p3: string,
6767
p4: string,
68-
) => { prefix: string; source: string; suffix: string };
68+
) => { prefix: string; source: string; suffix: string; };
6969

7070
// provide a (text) envelope around the capsule, e.g.
7171
// - newlines to ensure that yaml is parsed as a standalone paragraph;
@@ -88,6 +88,15 @@ export interface PandocBlockCapsuleFilter {
8888
writeNode: (schema: Schema, writer: ProsemirrorWriter, capsule: PandocBlockCapsule) => void;
8989
}
9090

91+
// default extractor
92+
const defaultExtractor = (_match: string, p1: string, p2: string, p3: string) => {
93+
return {
94+
prefix: p1,
95+
source: p2,
96+
suffix: p3,
97+
};
98+
};
99+
91100
// transform the passed markdown to include base64 encoded block capsules as specified by the
92101
// provided capsule filter. capsules are used to hoist block types that we don't want pandoc
93102
// to see (e.g. yaml metadata or Rmd chunks) out of the markdown, only to be re-inserted
@@ -99,15 +108,6 @@ export function pandocMarkdownWithBlockCapsules(
99108
markdown: string,
100109
capsuleFilter: PandocBlockCapsuleFilter,
101110
) {
102-
// default extractor
103-
const defaultExtractor = (_match: string, p1: string, p2: string, p3: string) => {
104-
return {
105-
prefix: p1,
106-
source: p2,
107-
suffix: p3,
108-
};
109-
};
110-
111111
// find the original position of all the matches
112112
const positions: number[] = [];
113113
let match = capsuleFilter.match.exec(original);
@@ -179,41 +179,27 @@ export function pandocMarkdownWithBlockCapsules(
179179
});
180180
}
181181

182-
// block capsules can also end up not as block tokens, but rather as text within another
183-
// token (e.g. within a backtick code block or raw_block). this function takes a set
184-
// of pandoc tokens and recursively converts block capsules that aren't of type
185-
// PandocTokenType.Str (which is what we'd see in a paragraph) into their original form
186-
export function resolvePandocBlockCapsuleText(
187-
tokens: PandocToken[],
188-
filters: readonly PandocBlockCapsuleFilter[],
189-
): PandocToken[] {
190-
// process all tokens
191-
return mapTokens(tokens, token => {
192-
// look for non-string pandoc tokens
193-
if (token.t !== PandocTokenType.Str && token.c) {
194-
if (typeof token.c === 'string') {
195-
token.c = decodeBlockCapsuleText(token.c, token, filters);
196-
} else if (Array.isArray(token.c)) {
197-
const children = token.c.length;
198-
for (let i = 0; i < children; i++) {
199-
if (typeof token.c[i] === 'string') {
200-
token.c[i] = decodeBlockCapsuleText(token.c[i], token, filters);
201-
}
202-
}
203-
}
204-
}
205-
206-
return token;
207-
});
208-
}
209-
210182
// decode the text capsule by running all of the filters (as there could be nesting)
211183
export function decodeBlockCapsuleText(text: string, tok: PandocToken, filters: readonly PandocBlockCapsuleFilter[]) {
212184
filters.forEach(filter => {
213185
text = filter.handleText(text, tok);
214186
});
215187
return text;
216188
}
189+
const resolveTokenBlockCapsuleText = (token: PandocToken, filters: readonly PandocBlockCapsuleFilter[]) => ({
190+
t: token.t,
191+
c: token.t !== PandocTokenType.Str && typeof token.c === 'string' ?
192+
decodeBlockCapsuleText(token.c, token, filters) :
193+
token.c
194+
});
195+
// block capsules can also end up not as block tokens, but rather as text within another
196+
// token (e.g. within a backtick code block or raw_block). this function takes a set
197+
// of pandoc tokens and recursively converts block capsules that aren't of type
198+
// PandocTokenType.Str (which is what we'd see in a paragraph) into their original form
199+
export const resolvePandocBlockCapsuleText = (
200+
tokens: PandocToken[],
201+
filters: readonly PandocBlockCapsuleFilter[],
202+
) => mapTokensRecursive(tokens, token => resolveTokenBlockCapsuleText(token, filters));
217203

218204
export function blockCapsuleTextHandler(type: string, pattern: RegExp, textFilter?: (text: string) => string) {
219205
return (text: string, tok: PandocToken): string => {
@@ -263,12 +249,12 @@ export function blockCapsuleParagraphTokenHandler(type: string) {
263249
export function encodedBlockCapsuleRegex(prefix?: string, suffix?: string, flags?: string) {
264250
return new RegExp(
265251
(prefix || '') +
266-
kBlockCapsuleSentinel +
267-
kValueDelimiter +
268-
'((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)' +
269-
kValueDelimiter +
270-
kBlockCapsuleSentinel +
271-
(suffix || ''),
252+
kBlockCapsuleSentinel +
253+
kValueDelimiter +
254+
'((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)' +
255+
kValueDelimiter +
256+
kBlockCapsuleSentinel +
257+
(suffix || ''),
272258
flags,
273259
);
274260
}

packages/editor/src/nodes/list/list-checked.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import { findParentNodeOfType, NodeWithPos, setTextSelection } from 'prosemirror
2020
import { InputRule, wrappingInputRule } from 'prosemirror-inputrules';
2121

2222
import { ProsemirrorCommand, EditorCommandId } from '../../api/command';
23-
import { PandocToken, mapTokens } from '../../api/pandoc';
23+
import { PandocToken, mapTokensRecursive } from '../../api/pandoc';
2424

2525
// custom NodeView that accomodates display / interaction with item check boxes
2626
export class CheckedListItemNodeView implements NodeView {
2727
public readonly dom: HTMLElement;
2828
public readonly contentDOM: HTMLElement;
2929

3030
constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {
31-
31+
3232
// create root li element
3333
this.dom = window.document.createElement('li');
3434
if (node.attrs.tight) {
@@ -167,7 +167,7 @@ export function checkedListItemInputRule() {
167167
}
168168

169169
export interface InputRuleWithHandler extends InputRule {
170-
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction
170+
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number) => Transaction;
171171
}
172172

173173
// allow users to begin a new checked list by typing [x] or [ ] at the beginning of a line
@@ -212,13 +212,13 @@ export function fragmentWithCheck(schema: Schema, fragment: Fragment, checked: b
212212
const kCheckedChar = '☒';
213213
const kUncheckedChar = '☐';
214214

215-
export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[] } {
215+
export function tokensWithChecked(tokens: PandocToken[]): { checked: null | boolean; tokens: PandocToken[]; } {
216216
// will set this flag based on inspecting the first Str token
217217
let checked: null | boolean | undefined;
218218
let lastWasChecked = false;
219219

220220
// map tokens
221-
const mappedTokens = mapTokens(tokens, tok => {
221+
const mappedTokens = mapTokensRecursive(tokens, tok => {
222222
// if the last token was checked then strip the next space
223223
if (tok.t === 'Space' && lastWasChecked) {
224224
lastWasChecked = false;

0 commit comments

Comments
 (0)