@tiptap/core 3.6.6 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +3653 -3143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +736 -63
- package/dist/index.d.ts +736 -63
- package/dist/index.js +3681 -3181
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Extendable.ts +44 -0
- package/src/ExtensionManager.ts +9 -0
- package/src/commands/insertContent.ts +15 -13
- package/src/commands/insertContentAt.ts +28 -26
- package/src/commands/setContent.ts +20 -18
- package/src/index.ts +3 -0
- package/src/pasteRules/nodePasteRule.ts +1 -1
- package/src/types.ts +156 -0
- package/src/utilities/index.ts +2 -0
- package/src/utilities/markdown/attributeUtils.ts +130 -0
- package/src/utilities/markdown/createAtomBlockMarkdownSpec.ts +141 -0
- package/src/utilities/markdown/createBlockMarkdownSpec.ts +225 -0
- package/src/utilities/markdown/createInlineMarkdownSpec.ts +236 -0
- package/src/utilities/markdown/index.ts +13 -0
- package/src/utilities/markdown/parseIndentedBlocks.ts +193 -0
- package/src/utilities/markdown/renderNestedMarkdownContent.ts +94 -0
- package/dist/jsx-runtime/jsx-runtime.d.cts +0 -23
- package/dist/jsx-runtime/jsx-runtime.d.ts +0 -23
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/core",
|
|
3
3
|
"description": "headless rich text editor",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.7.0",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"jsx-dev-runtime"
|
|
53
53
|
],
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@tiptap/pm": "^3.
|
|
55
|
+
"@tiptap/pm": "^3.7.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@tiptap/pm": "^3.
|
|
58
|
+
"@tiptap/pm": "^3.7.0"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
package/src/Extendable.ts
CHANGED
|
@@ -12,9 +12,16 @@ import type {
|
|
|
12
12
|
EditorEvents,
|
|
13
13
|
Extensions,
|
|
14
14
|
GlobalAttributes,
|
|
15
|
+
JSONContent,
|
|
15
16
|
KeyboardShortcutCommand,
|
|
17
|
+
MarkdownParseHelpers,
|
|
18
|
+
MarkdownParseResult,
|
|
19
|
+
MarkdownRendererHelpers,
|
|
20
|
+
MarkdownToken,
|
|
21
|
+
MarkdownTokenizer,
|
|
16
22
|
ParentConfig,
|
|
17
23
|
RawCommands,
|
|
24
|
+
RenderContext,
|
|
18
25
|
} from './types.js'
|
|
19
26
|
import { callOrReturn } from './utilities/callOrReturn.js'
|
|
20
27
|
import { mergeDeep } from './utilities/mergeDeep.js'
|
|
@@ -224,6 +231,43 @@ export interface ExtendableConfig<
|
|
|
224
231
|
parent: ParentConfig<Config>['addExtensions']
|
|
225
232
|
}) => Extensions
|
|
226
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The markdown token name
|
|
236
|
+
*
|
|
237
|
+
* This is the name of the token that this extension uses to parse and render markdown and comes from the Marked Lexer.
|
|
238
|
+
*
|
|
239
|
+
* @see https://github.com/markedjs/marked/blob/master/src/Tokens.ts
|
|
240
|
+
*
|
|
241
|
+
*/
|
|
242
|
+
markdownTokenName?: string
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The parse function used by the markdown parser to convert markdown tokens to ProseMirror nodes.
|
|
246
|
+
*/
|
|
247
|
+
parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The serializer function used by the markdown serializer to convert ProseMirror nodes to markdown tokens.
|
|
251
|
+
*/
|
|
252
|
+
renderMarkdown?: (node: JSONContent, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The markdown tokenizer responsible for turning a markdown string into tokens
|
|
256
|
+
*
|
|
257
|
+
* Custom tokenizers are only needed when you want to parse non-standard markdown token.
|
|
258
|
+
*/
|
|
259
|
+
markdownTokenizer?: MarkdownTokenizer
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Optional markdown options for indentation
|
|
263
|
+
*/
|
|
264
|
+
markdownOptions?: {
|
|
265
|
+
/**
|
|
266
|
+
* Defines if this markdown element should indent it's child elements
|
|
267
|
+
*/
|
|
268
|
+
indentsContent?: boolean
|
|
269
|
+
}
|
|
270
|
+
|
|
227
271
|
/**
|
|
228
272
|
* This function extends the schema of the node.
|
|
229
273
|
* @example
|
package/src/ExtensionManager.ts
CHANGED
|
@@ -29,12 +29,21 @@ export class ExtensionManager {
|
|
|
29
29
|
|
|
30
30
|
schema: Schema
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A flattened and sorted array of all extensions
|
|
34
|
+
*/
|
|
32
35
|
extensions: Extensions
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A non-flattened array of base extensions (no sub-extensions)
|
|
39
|
+
*/
|
|
40
|
+
baseExtensions: Extensions
|
|
41
|
+
|
|
34
42
|
splittableMarks: string[] = []
|
|
35
43
|
|
|
36
44
|
constructor(extensions: Extensions, editor: Editor) {
|
|
37
45
|
this.editor = editor
|
|
46
|
+
this.baseExtensions = extensions
|
|
38
47
|
this.extensions = resolveExtensions(extensions)
|
|
39
48
|
this.schema = getSchemaByResolvedExtensions(this.extensions, editor)
|
|
40
49
|
this.setupExtensions()
|
|
@@ -2,6 +2,20 @@ import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm
|
|
|
2
2
|
|
|
3
3
|
import type { Content, RawCommands } from '../types.js'
|
|
4
4
|
|
|
5
|
+
export interface InsertContentOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Options for parsing the content.
|
|
8
|
+
*/
|
|
9
|
+
parseOptions?: ParseOptions
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether to update the selection after inserting the content.
|
|
13
|
+
*/
|
|
14
|
+
updateSelection?: boolean
|
|
15
|
+
applyInputRules?: boolean
|
|
16
|
+
applyPasteRules?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
declare module '@tiptap/core' {
|
|
6
20
|
interface Commands<ReturnType> {
|
|
7
21
|
insertContent: {
|
|
@@ -19,19 +33,7 @@ declare module '@tiptap/core' {
|
|
|
19
33
|
/**
|
|
20
34
|
* Optional options
|
|
21
35
|
*/
|
|
22
|
-
options?:
|
|
23
|
-
/**
|
|
24
|
-
* Options for parsing the content.
|
|
25
|
-
*/
|
|
26
|
-
parseOptions?: ParseOptions
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Whether to update the selection after inserting the content.
|
|
30
|
-
*/
|
|
31
|
-
updateSelection?: boolean
|
|
32
|
-
applyInputRules?: boolean
|
|
33
|
-
applyPasteRules?: boolean
|
|
34
|
-
},
|
|
36
|
+
options?: InsertContentOptions,
|
|
35
37
|
) => ReturnType
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -5,6 +5,33 @@ import { createNodeFromContent } from '../helpers/createNodeFromContent.js'
|
|
|
5
5
|
import { selectionToInsertionEnd } from '../helpers/selectionToInsertionEnd.js'
|
|
6
6
|
import type { Content, Range, RawCommands } from '../types.js'
|
|
7
7
|
|
|
8
|
+
export interface InsertContentAtOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Options for parsing the content.
|
|
11
|
+
*/
|
|
12
|
+
parseOptions?: ParseOptions
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether to update the selection after inserting the content.
|
|
16
|
+
*/
|
|
17
|
+
updateSelection?: boolean
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether to apply input rules after inserting the content.
|
|
21
|
+
*/
|
|
22
|
+
applyInputRules?: boolean
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether to apply paste rules after inserting the content.
|
|
26
|
+
*/
|
|
27
|
+
applyPasteRules?: boolean
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to throw an error if the content is invalid.
|
|
31
|
+
*/
|
|
32
|
+
errorOnInvalidContent?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
declare module '@tiptap/core' {
|
|
9
36
|
interface Commands<ReturnType> {
|
|
10
37
|
insertContentAt: {
|
|
@@ -26,32 +53,7 @@ declare module '@tiptap/core' {
|
|
|
26
53
|
/**
|
|
27
54
|
* Optional options
|
|
28
55
|
*/
|
|
29
|
-
options?:
|
|
30
|
-
/**
|
|
31
|
-
* Options for parsing the content.
|
|
32
|
-
*/
|
|
33
|
-
parseOptions?: ParseOptions
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Whether to update the selection after inserting the content.
|
|
37
|
-
*/
|
|
38
|
-
updateSelection?: boolean
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Whether to apply input rules after inserting the content.
|
|
42
|
-
*/
|
|
43
|
-
applyInputRules?: boolean
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Whether to apply paste rules after inserting the content.
|
|
47
|
-
*/
|
|
48
|
-
applyPasteRules?: boolean
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Whether to throw an error if the content is invalid.
|
|
52
|
-
*/
|
|
53
|
-
errorOnInvalidContent?: boolean
|
|
54
|
-
},
|
|
56
|
+
options?: InsertContentAtOptions,
|
|
55
57
|
) => ReturnType
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -3,6 +3,25 @@ import type { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm
|
|
|
3
3
|
import { createDocument } from '../helpers/createDocument.js'
|
|
4
4
|
import type { Content, RawCommands } from '../types.js'
|
|
5
5
|
|
|
6
|
+
export interface SetContentOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Options for parsing the content.
|
|
9
|
+
* @default {}
|
|
10
|
+
*/
|
|
11
|
+
parseOptions?: ParseOptions
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to throw an error if the content is invalid.
|
|
15
|
+
*/
|
|
16
|
+
errorOnInvalidContent?: boolean
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether to emit an update event.
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
emitUpdate?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
declare module '@tiptap/core' {
|
|
7
26
|
interface Commands<ReturnType> {
|
|
8
27
|
setContent: {
|
|
@@ -22,24 +41,7 @@ declare module '@tiptap/core' {
|
|
|
22
41
|
/**
|
|
23
42
|
* Options for `setContent`.
|
|
24
43
|
*/
|
|
25
|
-
options?:
|
|
26
|
-
/**
|
|
27
|
-
* Options for parsing the content.
|
|
28
|
-
* @default {}
|
|
29
|
-
*/
|
|
30
|
-
parseOptions?: ParseOptions
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Whether to throw an error if the content is invalid.
|
|
34
|
-
*/
|
|
35
|
-
errorOnInvalidContent?: boolean
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether to emit an update event.
|
|
39
|
-
* @default true
|
|
40
|
-
*/
|
|
41
|
-
emitUpdate?: boolean
|
|
42
|
-
},
|
|
44
|
+
options?: SetContentOptions,
|
|
43
45
|
) => ReturnType
|
|
44
46
|
}
|
|
45
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from './CommandManager.js'
|
|
2
|
+
export type * from './commands/index.js'
|
|
3
|
+
export * as commands from './commands/index.js'
|
|
2
4
|
export * from './Editor.js'
|
|
5
|
+
export * from './Extendable.js'
|
|
3
6
|
export * from './Extension.js'
|
|
4
7
|
export * as extensions from './extensions/index.js'
|
|
5
8
|
export * from './helpers/index.js'
|
|
@@ -8,7 +8,7 @@ import { callOrReturn } from '../utilities/index.js'
|
|
|
8
8
|
/**
|
|
9
9
|
* Build an paste rule that adds a node when the
|
|
10
10
|
* matched text is pasted into it.
|
|
11
|
-
* @see https://tiptap.dev/docs/editor/
|
|
11
|
+
* @see https://tiptap.dev/docs/editor/api/paste-rules
|
|
12
12
|
*/
|
|
13
13
|
export function nodePasteRule(config: {
|
|
14
14
|
find: PasteRuleFinder
|
package/src/types.ts
CHANGED
|
@@ -755,3 +755,159 @@ export type ExtendedRegExpMatchArray = RegExpMatchArray & {
|
|
|
755
755
|
}
|
|
756
756
|
|
|
757
757
|
export type Dispatch = ((args?: any) => any) | undefined
|
|
758
|
+
|
|
759
|
+
/** Markdown related types */
|
|
760
|
+
|
|
761
|
+
// Shared markdown-related types for the MarkdownManager and extensions.
|
|
762
|
+
export type MarkdownToken = {
|
|
763
|
+
type?: string
|
|
764
|
+
raw?: string
|
|
765
|
+
text?: string
|
|
766
|
+
tokens?: MarkdownToken[]
|
|
767
|
+
depth?: number
|
|
768
|
+
items?: MarkdownToken[]
|
|
769
|
+
[key: string]: any
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export type MarkdownHelpers = {
|
|
773
|
+
// When used during parsing these helpers return JSON-like node objects
|
|
774
|
+
// (not ProseMirror Node instances). Use `any` to represent that shape.
|
|
775
|
+
parseInline: (tokens: MarkdownToken[]) => any[]
|
|
776
|
+
/**
|
|
777
|
+
* Render children. The second argument may be a legacy separator string
|
|
778
|
+
* or a RenderContext (preferred).
|
|
779
|
+
*/
|
|
780
|
+
renderChildren: (node: Node[] | Node, ctxOrSeparator?: RenderContext | string) => string
|
|
781
|
+
text: (token: MarkdownToken) => any
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Helpers specifically for parsing markdown tokens into Tiptap JSON.
|
|
786
|
+
* These are provided to extension parse handlers.
|
|
787
|
+
*/
|
|
788
|
+
export type MarkdownParseHelpers = {
|
|
789
|
+
/** Parse an array of inline tokens into text nodes with marks */
|
|
790
|
+
parseInline: (tokens: MarkdownToken[]) => JSONContent[]
|
|
791
|
+
/** Parse an array of block-level tokens */
|
|
792
|
+
parseChildren: (tokens: MarkdownToken[]) => JSONContent[]
|
|
793
|
+
/** Create a text node with optional marks */
|
|
794
|
+
createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => JSONContent
|
|
795
|
+
/** Create any node type with attributes and content */
|
|
796
|
+
createNode: (type: string, attrs?: any, content?: JSONContent[]) => JSONContent
|
|
797
|
+
/** Apply a mark to content (used for inline marks like bold, italic) */
|
|
798
|
+
applyMark: (
|
|
799
|
+
markType: string,
|
|
800
|
+
content: JSONContent[],
|
|
801
|
+
attrs?: any,
|
|
802
|
+
) => { mark: string; content: JSONContent[]; attrs?: any }
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Full runtime helpers object provided by MarkdownManager to handlers.
|
|
807
|
+
* This includes the small author-facing helpers plus internal helpers
|
|
808
|
+
* that can be useful for advanced handlers.
|
|
809
|
+
*/
|
|
810
|
+
export type FullMarkdownHelpers = MarkdownHelpers & {
|
|
811
|
+
// parseChildren returns JSON-like nodes when invoked during parsing.
|
|
812
|
+
parseChildren: (tokens: MarkdownToken[]) => any[]
|
|
813
|
+
getExtension: (name: string) => any
|
|
814
|
+
// createNode returns a JSON-like node during parsing; render-time helpers
|
|
815
|
+
// may instead work with real ProseMirror Node instances.
|
|
816
|
+
createNode: (type: string, attrs?: any, content?: any[]) => any
|
|
817
|
+
/** Current render context when calling renderers; undefined during parse. */
|
|
818
|
+
currentContext?: RenderContext
|
|
819
|
+
/** Indent a multi-line string according to the provided RenderContext. */
|
|
820
|
+
indent: (text: string, ctx?: RenderContext) => string
|
|
821
|
+
/** Return the indent string for a given level (e.g. ' ' or '\t'). */
|
|
822
|
+
getIndentString: (level?: number) => string
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export default MarkdownHelpers
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Return shape for parser-level `parse` handlers.
|
|
829
|
+
* - a single JSON-like node
|
|
830
|
+
* - an array of JSON-like nodes
|
|
831
|
+
* - or a `{ mark: string, content: JSONLike[] }` shape to apply a mark
|
|
832
|
+
*/
|
|
833
|
+
export type MarkdownParseResult = JSONContent | JSONContent[] | { mark: string; content: JSONContent[]; attrs?: any }
|
|
834
|
+
|
|
835
|
+
export type RenderContext = {
|
|
836
|
+
index: number
|
|
837
|
+
level: number
|
|
838
|
+
meta?: Record<string, any>
|
|
839
|
+
parentType?: string | null
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Extension contract for markdown parsing/serialization. */
|
|
843
|
+
export interface MarkdownExtensionSpec {
|
|
844
|
+
/** Token name used for parsing (e.g., 'codespan', 'code', 'strong') */
|
|
845
|
+
tokenName?: string
|
|
846
|
+
/** Node/mark name used for rendering (typically the extension name) */
|
|
847
|
+
nodeName?: string
|
|
848
|
+
parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult
|
|
849
|
+
renderMarkdown?: (node: any, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string
|
|
850
|
+
isIndenting?: boolean
|
|
851
|
+
/** Custom tokenizer for marked.js to handle non-standard markdown syntax */
|
|
852
|
+
tokenizer?: MarkdownTokenizer
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Configuration object passed to custom marked.js tokenizers
|
|
857
|
+
*/
|
|
858
|
+
export type MarkdownLexerConfiguration = {
|
|
859
|
+
/**
|
|
860
|
+
* Can be used to transform source text into inline tokens - useful while tokenizing child tokens.
|
|
861
|
+
* @param src
|
|
862
|
+
* @returns Array of inline tokens
|
|
863
|
+
*/
|
|
864
|
+
inlineTokens: (src: string) => MarkdownToken[]
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Can be used to transform source text into block-level tokens - useful while tokenizing child tokens.
|
|
868
|
+
* @param src
|
|
869
|
+
* @returns Array of block-level tokens
|
|
870
|
+
*/
|
|
871
|
+
blockTokens: (src: string) => MarkdownToken[]
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Custom tokenizer function for marked.js extensions */
|
|
875
|
+
export type MarkdownTokenizer = {
|
|
876
|
+
/** Token name this tokenizer creates */
|
|
877
|
+
name: string
|
|
878
|
+
/** Priority level for tokenizer ordering (higher = earlier) */
|
|
879
|
+
level?: 'block' | 'inline'
|
|
880
|
+
/** A string to look for or a function that returns the start index of the token in the source string */
|
|
881
|
+
start?: string | ((src: string) => number)
|
|
882
|
+
/** Function that attempts to parse custom syntax from start of text */
|
|
883
|
+
tokenize: (
|
|
884
|
+
src: string,
|
|
885
|
+
tokens: MarkdownToken[],
|
|
886
|
+
lexer: MarkdownLexerConfiguration,
|
|
887
|
+
) => MarkdownToken | undefined | void
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export type MarkdownRendererHelpers = {
|
|
891
|
+
/**
|
|
892
|
+
* Render children nodes to a markdown string, optionally separated by a string.
|
|
893
|
+
* @param nodes The node or array of nodes to render
|
|
894
|
+
* @param separator An optional separator string (legacy) or RenderContext
|
|
895
|
+
* @returns The rendered markdown string
|
|
896
|
+
*/
|
|
897
|
+
renderChildren: (nodes: JSONContent | JSONContent[], separator?: string) => string
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Render a text token to a markdown string
|
|
901
|
+
* @param prefix The prefix to add before the content
|
|
902
|
+
* @param content The content to wrap
|
|
903
|
+
* @returns The wrapped content
|
|
904
|
+
*/
|
|
905
|
+
wrapInBlock: (prefix: string, content: string) => string
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Indent a markdown string according to the provided RenderContext
|
|
909
|
+
* @param content The content to indent
|
|
910
|
+
* @returns The indented content
|
|
911
|
+
*/
|
|
912
|
+
indent: (content: string) => string
|
|
913
|
+
}
|
package/src/utilities/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export * from './isNumber.js'
|
|
|
15
15
|
export * from './isPlainObject.js'
|
|
16
16
|
export * from './isRegExp.js'
|
|
17
17
|
export * from './isString.js'
|
|
18
|
+
export * from './markdown/index.js'
|
|
19
|
+
export * as markdown from './markdown/index.js'
|
|
18
20
|
export * from './mergeAttributes.js'
|
|
19
21
|
export * from './mergeDeep.js'
|
|
20
22
|
export * from './minMax.js'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility functions for parsing and serializing markdown attributes.
|
|
3
|
+
*
|
|
4
|
+
* These utilities handle the common patterns for parsing attribute strings
|
|
5
|
+
* in various markdown syntaxes like Pandoc attributes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses a Pandoc-style attribute string into an object.
|
|
10
|
+
*
|
|
11
|
+
* Supports the following patterns:
|
|
12
|
+
* - Classes: `.className` → `{ class: 'className' }`
|
|
13
|
+
* - IDs: `#myId` → `{ id: 'myId' }`
|
|
14
|
+
* - Key-value pairs: `key="value"` → `{ key: 'value' }`
|
|
15
|
+
* - Boolean attributes: `disabled` → `{ disabled: true }`
|
|
16
|
+
*
|
|
17
|
+
* @param attrString - The attribute string to parse
|
|
18
|
+
* @returns Parsed attributes object
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* parseAttributes('.btn #submit disabled type="button"')
|
|
23
|
+
* // → { class: 'btn', id: 'submit', disabled: true, type: 'button' }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function parseAttributes(attrString: string): Record<string, any> {
|
|
27
|
+
if (!attrString?.trim()) {
|
|
28
|
+
return {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const attributes: Record<string, any> = {}
|
|
32
|
+
|
|
33
|
+
// First, extract and remove quoted strings to avoid parsing content inside them
|
|
34
|
+
const quotedStrings: string[] = []
|
|
35
|
+
const tempString = attrString.replace(/["']([^"']*)["']/g, match => {
|
|
36
|
+
quotedStrings.push(match)
|
|
37
|
+
return `__QUOTED_${quotedStrings.length - 1}__`
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Parse classes (.className) - only outside of quoted strings
|
|
41
|
+
const classMatches = tempString.match(/(?:^|\s)\.([a-zA-Z][\w-]*)/g)
|
|
42
|
+
if (classMatches) {
|
|
43
|
+
const classes = classMatches.map(match => match.trim().slice(1)) // Remove the dot
|
|
44
|
+
attributes.class = classes.join(' ')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse IDs (#myId) - only outside of quoted strings
|
|
48
|
+
const idMatch = tempString.match(/(?:^|\s)#([a-zA-Z][\w-]*)/)
|
|
49
|
+
if (idMatch) {
|
|
50
|
+
attributes.id = idMatch[1]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse key-value pairs (key="value" or key='value') - restore quoted strings
|
|
54
|
+
const kvRegex = /([a-zA-Z][\w-]*)\s*=\s*(__QUOTED_\d+__)/g
|
|
55
|
+
const kvMatches = Array.from(tempString.matchAll(kvRegex))
|
|
56
|
+
kvMatches.forEach(([, key, quotedRef]) => {
|
|
57
|
+
const quotedIndex = parseInt(quotedRef.match(/__QUOTED_(\d+)__/)?.[1] || '0', 10)
|
|
58
|
+
const quotedValue = quotedStrings[quotedIndex]
|
|
59
|
+
if (quotedValue) {
|
|
60
|
+
// Remove the outer quotes
|
|
61
|
+
attributes[key] = quotedValue.slice(1, -1)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Parse boolean attributes (standalone words that aren't classes/IDs)
|
|
66
|
+
const cleanString = tempString
|
|
67
|
+
.replace(/(?:^|\s)\.([a-zA-Z][\w-]*)/g, '') // Remove classes
|
|
68
|
+
.replace(/(?:^|\s)#([a-zA-Z][\w-]*)/g, '') // Remove IDs
|
|
69
|
+
.replace(/([a-zA-Z][\w-]*)\s*=\s*__QUOTED_\d+__/g, '') // Remove key-value pairs
|
|
70
|
+
.trim()
|
|
71
|
+
|
|
72
|
+
if (cleanString) {
|
|
73
|
+
const booleanAttrs = cleanString.split(/\s+/).filter(Boolean)
|
|
74
|
+
booleanAttrs.forEach(attr => {
|
|
75
|
+
if (attr.match(/^[a-zA-Z][\w-]*$/)) {
|
|
76
|
+
attributes[attr] = true
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return attributes
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Serializes an attributes object back to a Pandoc-style attribute string.
|
|
86
|
+
*
|
|
87
|
+
* @param attributes - The attributes object to serialize
|
|
88
|
+
* @returns Serialized attribute string
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* serializeAttributes({ class: 'btn primary', id: 'submit', disabled: true, type: 'button' })
|
|
93
|
+
* // → '.btn.primary #submit disabled type="button"'
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function serializeAttributes(attributes: Record<string, any>): string {
|
|
97
|
+
if (!attributes || Object.keys(attributes).length === 0) {
|
|
98
|
+
return ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts: string[] = []
|
|
102
|
+
|
|
103
|
+
// Handle classes
|
|
104
|
+
if (attributes.class) {
|
|
105
|
+
const classes = String(attributes.class).split(/\s+/).filter(Boolean)
|
|
106
|
+
classes.forEach(cls => parts.push(`.${cls}`))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle ID
|
|
110
|
+
if (attributes.id) {
|
|
111
|
+
parts.push(`#${attributes.id}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle other attributes
|
|
115
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
116
|
+
if (key === 'class' || key === 'id') {
|
|
117
|
+
return // Already handled
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (value === true) {
|
|
121
|
+
// Boolean attribute
|
|
122
|
+
parts.push(key)
|
|
123
|
+
} else if (value !== false && value != null) {
|
|
124
|
+
// Key-value attribute
|
|
125
|
+
parts.push(`${key}="${String(value)}"`)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return parts.join(' ')
|
|
130
|
+
}
|