svelte-streamdown 4.1.0 → 4.2.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.
Files changed (37) hide show
  1. package/README.md +149 -47
  2. package/dist/Block.svelte +19 -8
  3. package/dist/Block.svelte.d.ts +2 -0
  4. package/dist/Elements/Code.svelte +15 -4
  5. package/dist/Elements/Code.svelte.d.ts +2 -0
  6. package/dist/Elements/Element.svelte +32 -11
  7. package/dist/Elements/Element.svelte.d.ts +1 -0
  8. package/dist/Elements/Mermaid.svelte +5 -11
  9. package/dist/Elements/Mermaid.svelte.d.ts +1 -0
  10. package/dist/Elements/TableDownload.svelte +44 -3
  11. package/dist/Elements/TableDownload.svelte.d.ts +3 -1
  12. package/dist/Elements/fallbacks/CodeFallback.svelte +16 -6
  13. package/dist/Elements/fallbacks/CodeFallback.svelte.d.ts +3 -2
  14. package/dist/Elements/icons.d.ts +1 -0
  15. package/dist/Elements/icons.js +8 -0
  16. package/dist/Streamdown.svelte +68 -6
  17. package/dist/context.svelte.d.ts +37 -1
  18. package/dist/context.svelte.js +3 -0
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +2 -1
  21. package/dist/marked/index.d.ts +11 -4
  22. package/dist/marked/index.js +54 -45
  23. package/dist/marked/marked-mdx.d.ts +27 -1
  24. package/dist/marked/marked-mdx.js +37 -8
  25. package/dist/theme.d.ts +6 -0
  26. package/dist/theme.js +12 -4
  27. package/dist/utils/expand.svelte.d.ts +21 -0
  28. package/dist/utils/expand.svelte.js +46 -0
  29. package/dist/utils/fence.d.ts +12 -1
  30. package/dist/utils/fence.js +34 -17
  31. package/dist/utils/line-numbers.d.ts +23 -0
  32. package/dist/utils/line-numbers.js +30 -0
  33. package/dist/utils/normalize-html-indentation.d.ts +10 -0
  34. package/dist/utils/normalize-html-indentation.js +52 -0
  35. package/dist/utils/parse-incomplete-markdown.d.ts +20 -6
  36. package/dist/utils/parse-incomplete-markdown.js +173 -69
  37. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Four leading spaces after a blank line is an indented code block, HTML or not.
3
+ * A pretty-printed document therefore splits into html / code / html and the
4
+ * indented part renders as source in a code box (upstream 7f9127b).
5
+ *
6
+ * This strips the indentation off tag lines only. It is lossy inside `<pre>` and
7
+ * `<code>`, where whitespace is content, so those bodies are skipped and the
8
+ * whole thing is behind the opt-in `normalizeHtmlIndentation` prop.
9
+ */
10
+ // The indentation of a line whose first non-space character opens a tag, a
11
+ // closing tag, or a comment/doctype. Anchored, and the tag itself is a lookahead
12
+ // so the match is exactly the whitespace to drop.
13
+ const INDENTED_TAG = /^[ \t]+(?=<[a-zA-Z!/])/;
14
+ // Inside these, indentation is content. `<pre class=…>` and `<code>` both match.
15
+ const LITERAL_OPEN = /<(?:pre|code)[\s/>]/gi;
16
+ const LITERAL_CLOSE = /<\/(?:pre|code)>/gi;
17
+ const count = (line, re) => {
18
+ re.lastIndex = 0;
19
+ let n = 0;
20
+ while (re.exec(line))
21
+ n++;
22
+ return n;
23
+ };
24
+ export const normalizeHtmlIndentation = (content) => {
25
+ // Only documents that start with a tag; anything else is Markdown whose
26
+ // indentation we have no business touching. Returned by identity so a Svelte
27
+ // `$derived` over it does not churn.
28
+ let i = 0;
29
+ while (i < content.length && (content[i] === ' ' || content[i] === '\n' || content[i] === '\t'))
30
+ i++;
31
+ if (content[i] !== '<')
32
+ return content;
33
+ const lines = content.split('\n');
34
+ let literalDepth = 0;
35
+ let changed = false;
36
+ for (let l = 0; l < lines.length; l++) {
37
+ const line = lines[l];
38
+ if (literalDepth === 0) {
39
+ const match = INDENTED_TAG.exec(line);
40
+ if (match) {
41
+ lines[l] = line.slice(match[0].length);
42
+ changed = true;
43
+ }
44
+ }
45
+ if (line.indexOf('<') !== -1) {
46
+ literalDepth += count(line, LITERAL_OPEN) - count(line, LITERAL_CLOSE);
47
+ if (literalDepth < 0)
48
+ literalDepth = 0;
49
+ }
50
+ }
51
+ return changed ? lines.join('\n') : content;
52
+ };
@@ -1,4 +1,16 @@
1
1
  import { type OpenFence } from './fence.js';
2
+ import { type TagMatchers } from '../marked/marked-mdx.js';
3
+ /** Per-call knobs `Block.svelte` threads down from the Streamdown context. */
4
+ export type CompleterOptions = {
5
+ /** The tag allowlist the lexer uses, so the completer never disagrees with it. */
6
+ tags?: TagMatchers;
7
+ /**
8
+ * Is this the block still being streamed into? Only there may a half-typed
9
+ * HTML tag be stripped — a sealed paragraph reading `if a <b then` must keep
10
+ * its tail forever (upstream 3e6a77d strips unconditionally; we do not).
11
+ */
12
+ live?: boolean;
13
+ };
2
14
  export interface Plugin {
3
15
  name: string;
4
16
  pattern?: RegExp;
@@ -24,6 +36,8 @@ interface HandlerPayload {
24
36
  }
25
37
  interface ParseState {
26
38
  currentLine: number;
39
+ /** How many lines the text has, so a plugin can tell the last one. */
40
+ lineCount: number;
27
41
  context: 'normal' | 'list' | 'blockquote' | 'descriptionList';
28
42
  blockingContexts: Set<'code' | 'math' | 'center' | 'right'>;
29
43
  /** Delimiter that closes the open math block: '$$' or '\]'. */
@@ -40,18 +54,18 @@ interface ParseState {
40
54
  tagName: string;
41
55
  lineIndex: number;
42
56
  }>;
43
- mdxLineStates?: Array<{
44
- inMdx: boolean;
45
- incompletePositions: number[];
46
- }>;
57
+ /** Tag allowlist for this call (see CompleterOptions). */
58
+ tags: TagMatchers;
59
+ /** Is this the block still streaming (see CompleterOptions)? */
60
+ live: boolean;
47
61
  }
48
62
  export declare class IncompleteMarkdownParser {
49
63
  private plugins;
50
64
  private state;
51
65
  setState: (state: Partial<ParseState>) => void;
52
66
  constructor(plugins?: Plugin[]);
53
- parse(text: string): string;
67
+ parse(text: string, options?: CompleterOptions): string;
54
68
  static createDefaultPlugins(): Plugin[];
55
69
  }
56
- export declare const parseIncompleteMarkdown: (text: string) => string;
70
+ export declare const parseIncompleteMarkdown: (text: string, options?: CompleterOptions) => string;
57
71
  export {};
@@ -1,11 +1,15 @@
1
- import { trackFence } from './fence.js';
1
+ import { closingFence, trackFence } from './fence.js';
2
+ import { DEFAULT_TAGS } from '../marked/marked-mdx.js';
2
3
  export class IncompleteMarkdownParser {
3
4
  plugins = [];
4
5
  state = {
5
6
  currentLine: 0,
7
+ lineCount: 0,
6
8
  context: 'normal',
7
9
  blockingContexts: new Set(),
8
- lineContexts: []
10
+ lineContexts: [],
11
+ tags: DEFAULT_TAGS,
12
+ live: true
9
13
  };
10
14
  setState = (state) => {
11
15
  this.state = { ...this.state, ...state };
@@ -14,15 +18,18 @@ export class IncompleteMarkdownParser {
14
18
  this.plugins = plugins;
15
19
  }
16
20
  // Main parsing methods
17
- parse(text) {
21
+ parse(text, options) {
18
22
  if (!text || typeof text !== 'string') {
19
23
  return text;
20
24
  }
21
25
  this.state = {
22
26
  currentLine: 0,
27
+ lineCount: 0,
23
28
  context: 'normal',
24
29
  blockingContexts: new Set(),
25
- lineContexts: []
30
+ lineContexts: [],
31
+ tags: options?.tags ?? DEFAULT_TAGS,
32
+ live: options?.live ?? true
26
33
  };
27
34
  let result = text;
28
35
  // Execute preprocess hooks for all plugins
@@ -50,6 +57,7 @@ export class IncompleteMarkdownParser {
50
57
  // Split into lines for processing
51
58
  const lines = result.split('\n');
52
59
  const processedLines = [...lines];
60
+ this.state.lineCount = processedLines.length;
53
61
  // Process each line with each plugin
54
62
  for (let i = 0; i < processedLines.length; i++) {
55
63
  this.state.currentLine = i;
@@ -99,7 +107,59 @@ export class IncompleteMarkdownParser {
99
107
  static createDefaultPlugins() {
100
108
  return [
101
109
  {
102
- // Runs first: in a list item a '>' before a number is a comparison
110
+ // `Hello <div cla` and `<div>content</di` render as literal source on
111
+ // every chunk until the `>` lands (upstream 3e6a77d). Strip that tail —
112
+ // but only on the block still being streamed and only when the name is
113
+ // really a tag. Upstream strips `/<[a-zA-Z\/][^>]*$/` unconditionally,
114
+ // which on our per-block completer would eat the tail of a sealed
115
+ // paragraph reading `if a <b then` for good.
116
+ // Registered first so the mdx plugin below never sees a half-typed tag.
117
+ name: 'incompleteHtmlTag',
118
+ // Cheap gate: a line with no '<' can never match.
119
+ pattern: /</,
120
+ skipInBlockTypes: ['code', 'math'],
121
+ handler: ({ line, state }) => {
122
+ // Only the streaming tail: the live block's LAST line. An unfinished
123
+ // tag on an earlier line can never complete, and `live` is merely
124
+ // "last block", so `Use the <div element to wrap it.` in a finished
125
+ // document must keep its text. The patterns below are `$`-anchored,
126
+ // so matching on the last line means matching at the end of the text.
127
+ if (!state.live || state.currentLine !== state.lineCount - 1)
128
+ return line;
129
+ // The incomplete tag is the last '<' with no '>' after it, so anchor
130
+ // there instead of letting `[^>]*$` backtrack from every '<' on the
131
+ // line (that is quadratic on a line full of complete tags).
132
+ const start = line.lastIndexOf('<');
133
+ if (start === -1)
134
+ return line;
135
+ // `</` with the name not yet typed is unambiguous — no prose ends a
136
+ // line that way — and it is the one frame a name check cannot catch.
137
+ if (start === line.length - 2 && line[start + 1] === '/') {
138
+ return isWithinCompleteInlineCode(line, start) ? line : line.slice(0, start);
139
+ }
140
+ const match = incompleteHtmlTag.exec(line.slice(start));
141
+ if (!match)
142
+ return line;
143
+ const name = match[1];
144
+ if (!isTagPrefix(name, state.tags))
145
+ return line;
146
+ // One-letter lowercase elements (a, b, i, p, q, s, u) are also how prose
147
+ // looks: `if a <b then` on the last line of a finished document must keep
148
+ // its text, and `live` cannot tell finished from streaming. Such a letter
149
+ // only counts as a tag once an attribute has started (`<a href="`). An
150
+ // uppercase letter (`<C`) can only be a component prefix, never prose.
151
+ if (name.length === 1 && name === name.toLowerCase() && !match[0].includes('='))
152
+ return line;
153
+ // A '<' inside a closed code span is content, not a tag (a725579).
154
+ if (isWithinCompleteInlineCode(line, start))
155
+ return line;
156
+ // No trimEnd (upstream does): 4.1.1 cut PascalCase tags at exactly
157
+ // this offset, so `Some text <Comp` keeps rendering as `Some text `.
158
+ return line.slice(0, start);
159
+ }
160
+ },
161
+ {
162
+ // In a list item a '>' before a number is a comparison
103
163
  // ('- > 25: rich'), but marked reads it as a nested blockquote. Escaping it
104
164
  // keeps the text, and the escape renders as a plain '>' (4fffb9f).
105
165
  // `pattern` is only a cheap gate — a line whose first non-space character is
@@ -196,10 +256,12 @@ export class IncompleteMarkdownParser {
196
256
  // Close inner blocks (code/math) before alignment wrappers.
197
257
  let result = text;
198
258
  if (state.blockingContexts.has('code')) {
199
- // Close with the fence that was opened: a '~~~' block is not closed by
200
- // '```', and a longer run needs a closer at least as long.
259
+ // Close with the fence that was opened, at the depth it was opened: a
260
+ // '~~~' block is not closed by '```', a longer run needs a closer at
261
+ // least as long, and a fence inside a list item needs its closer inside
262
+ // the item — one at column 0 leaves the block open and starts a new one.
201
263
  const fence = state.openFence;
202
- result += '\n' + (fence ? fence.char.repeat(fence.length) : '```');
264
+ result += '\n' + (fence ? closingFence(fence) : '```');
203
265
  }
204
266
  if (state.blockingContexts.has('math')) {
205
267
  if (state.mathCloser === '\\]') {
@@ -841,24 +903,21 @@ export class IncompleteMarkdownParser {
841
903
  skipInBlockTypes: ['code', 'math', 'center', 'right'],
842
904
  preprocess: ({ text, state }) => {
843
905
  // Track MDX component states across the entire text
906
+ const tags = state.tags;
844
907
  const lines = text.split('\n');
845
908
  const openTags = [];
846
- let mdxLineStates = [];
847
909
  for (let i = 0; i < lines.length; i++) {
848
910
  // Lines inside code fences or math blocks are opaque content: MDX-looking
849
911
  // tags there must not open/close/track components.
850
912
  const lineCtx = state.lineContexts?.[i];
851
913
  if (lineCtx?.code || lineCtx?.math) {
852
- mdxLineStates[i] = { inMdx: false, incompletePositions: [] };
853
914
  continue;
854
915
  }
855
916
  const line = lines[i];
856
- let inMdx = false;
857
- let incompletePositions = [];
858
917
  // Find all MDX tags in the line
859
918
  let searchPos = 0;
860
919
  while (searchPos < line.length) {
861
- // Look for opening bracket with capital letter (MDX component)
920
+ // Look for an opening bracket that starts an allowlisted tag
862
921
  const tagStart = line.indexOf('<', searchPos);
863
922
  if (tagStart === -1 || tagStart >= line.length - 1)
864
923
  break;
@@ -866,7 +925,7 @@ export class IncompleteMarkdownParser {
866
925
  // Closing tag for a component opened on an earlier line. Handled
867
926
  // inside the scan so a close that is part of a same-line complete
868
927
  // pair (consumed below) is never double-counted against the stack.
869
- const closeTagMatch = line.substring(tagStart).match(/^<\/([A-Z][a-zA-Z0-9]*)>/);
928
+ const closeTagMatch = line.substring(tagStart).match(tags.closeTag);
870
929
  if (closeTagMatch) {
871
930
  const tagName = closeTagMatch[1];
872
931
  // Pop the innermost same-name open (LIFO) so the auto-appended
@@ -880,82 +939,38 @@ export class IncompleteMarkdownParser {
880
939
  searchPos = tagStart + closeTagMatch[0].length;
881
940
  continue;
882
941
  }
883
- // Only match if starts with capital letter (MDX component)
884
- if (!/[A-Z]/.test(nextChar)) {
942
+ // Every allowlisted name starts with a letter; cheap gate.
943
+ if (!/[a-zA-Z]/.test(nextChar)) {
885
944
  searchPos = tagStart + 1;
886
945
  continue;
887
946
  }
888
947
  // Try to match complete self-closing tag
889
- const selfClosingMatch = line
890
- .substring(tagStart)
891
- .match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*\/>/);
948
+ const selfClosingMatch = line.substring(tagStart).match(tags.selfClosing);
892
949
  if (selfClosingMatch) {
893
950
  searchPos = tagStart + selfClosingMatch[0].length;
894
951
  continue;
895
952
  }
896
953
  // Try to match complete opening tag with immediate closing
897
- const completeMatch = line
898
- .substring(tagStart)
899
- .match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*>.*?<\/\1>/);
954
+ const completeMatch = line.substring(tagStart).match(tags.complete);
900
955
  if (completeMatch) {
901
956
  searchPos = tagStart + completeMatch[0].length;
902
957
  continue;
903
958
  }
904
959
  // Try to match opening tag
905
- const openTagMatch = line
906
- .substring(tagStart)
907
- .match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*>/);
960
+ const openTagMatch = line.substring(tagStart).match(tags.openTag);
908
961
  if (openTagMatch) {
909
962
  const tagName = openTagMatch[1];
910
963
  openTags.push({ tagName, lineIndex: i });
911
- inMdx = true;
912
964
  searchPos = tagStart + openTagMatch[0].length;
913
965
  continue;
914
966
  }
915
- // Check for incomplete self-closing (e.g., <Component /)
916
- const incompleteSelfClosing = line
917
- .substring(tagStart)
918
- .match(/^<([A-Z][a-zA-Z0-9]*)[^>]*\/$/);
919
- if (incompleteSelfClosing) {
920
- incompletePositions.push(tagStart);
921
- break; // This is at the end of the line
922
- }
923
- // Check for incomplete tag (no closing >) - only at end of line
924
- const incompleteTag = line
925
- .substring(tagStart)
926
- .match(/^<([A-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?$/);
927
- if (incompleteTag) {
928
- incompletePositions.push(tagStart);
929
- break; // This is at the end of the line
930
- }
967
+ // A tag cut off before its `>` is the last thing on the line (the
968
+ // incompleteHtmlTag plugin, which ran first, strips it); scanning on
969
+ // from the next character finds nothing and ends the loop.
931
970
  searchPos = tagStart + 1;
932
971
  }
933
- mdxLineStates[i] = { inMdx, incompletePositions };
934
972
  }
935
- return {
936
- text,
937
- state: {
938
- mdxUnclosedTags: openTags,
939
- mdxLineStates
940
- }
941
- };
942
- },
943
- handler: ({ line, state }) => {
944
- // Remove incomplete MDX syntax (don't render it)
945
- const lineStates = state.mdxLineStates || [];
946
- const currentState = lineStates[state.currentLine];
947
- if (currentState?.incompletePositions && currentState.incompletePositions.length > 0) {
948
- // Process incomplete positions from right to left to preserve indices
949
- let result = line;
950
- for (let i = currentState.incompletePositions.length - 1; i >= 0; i--) {
951
- const pos = currentState.incompletePositions[i];
952
- const before = result.substring(0, pos);
953
- // Simply remove the incomplete MDX tag
954
- result = before;
955
- }
956
- return result;
957
- }
958
- return line;
973
+ return { text, state: { mdxUnclosedTags: openTags } };
959
974
  },
960
975
  postprocess: ({ text, state }) => {
961
976
  // Complete unclosed MDX components at the end
@@ -977,15 +992,104 @@ export class IncompleteMarkdownParser {
977
992
  // Legacy function for backward compatibility
978
993
  const defaultPlugins = IncompleteMarkdownParser.createDefaultPlugins();
979
994
  const defaultParser = new IncompleteMarkdownParser(defaultPlugins);
980
- export const parseIncompleteMarkdown = (text) => {
995
+ export const parseIncompleteMarkdown = (text, options) => {
981
996
  if (!text || typeof text !== 'string') {
982
997
  return text;
983
998
  }
984
- return defaultParser.parse(text);
999
+ return defaultParser.parse(text, options);
985
1000
  };
986
1001
  // Utility functions
987
1002
  // Full test for the comparisonOperator plugin, whose `pattern` only gates it.
988
1003
  const listItemComparison = /^(\s*(?:[-*+]|\d+[.)]) +)>(?==?\s*\$?\d)/;
1004
+ // An opening or closing tag that never got its '>', anchored at the line's last
1005
+ // '<': the name (hyphens allowed, so `</ai-thinking` is caught too), then any
1006
+ // number of COMPLETE attributes, then at most one still being typed. Upstream
1007
+ // accepts any `[^>]*` tail, which also swallows ordinary prose — `if a <b then
1008
+ // c` and `Use the <div element to wrap it.` both end in a `<name` with no '>'
1009
+ // after it. Requiring every attribute but the last to carry a quoted value is
1010
+ // what tells a half-typed tag from a sentence.
1011
+ const incompleteHtmlTag = /^<\/?([a-zA-Z][a-zA-Z0-9-]*)(?:\s+[\w-]+=(?:"[^"]*"|'[^']*'|\{[^}]*\}))*(?:\s+[\w-]*(?:=(?:"[^"]*|'[^']*|\{[^}]*)?)?)?\s*\/?$/;
1012
+ /**
1013
+ * The HTML element names a half-typed tag may be stripped for. Deliberately a
1014
+ * short list of what an LLM actually emits rather than the full HTML vocabulary:
1015
+ * every name here is one the user will never mean literally at the end of a
1016
+ * streamed line, which is what makes the strip safe. Anything else must come in
1017
+ * through `customTags` / `mdxComponents`.
1018
+ */
1019
+ const htmlElements = [
1020
+ 'a',
1021
+ 'abbr',
1022
+ 'b',
1023
+ 'blockquote',
1024
+ 'br',
1025
+ 'button',
1026
+ 'code',
1027
+ 'col',
1028
+ 'colgroup',
1029
+ 'dd',
1030
+ 'del',
1031
+ 'details',
1032
+ 'div',
1033
+ 'dl',
1034
+ 'dt',
1035
+ 'em',
1036
+ 'figcaption',
1037
+ 'figure',
1038
+ 'h1',
1039
+ 'h2',
1040
+ 'h3',
1041
+ 'h4',
1042
+ 'h5',
1043
+ 'h6',
1044
+ 'hr',
1045
+ 'i',
1046
+ 'iframe',
1047
+ 'img',
1048
+ 'input',
1049
+ 'ins',
1050
+ 'kbd',
1051
+ 'label',
1052
+ 'li',
1053
+ 'mark',
1054
+ 'ol',
1055
+ 'p',
1056
+ 'picture',
1057
+ 'pre',
1058
+ 'q',
1059
+ 's',
1060
+ 'samp',
1061
+ 'section',
1062
+ 'small',
1063
+ 'source',
1064
+ 'span',
1065
+ 'strong',
1066
+ 'sub',
1067
+ 'summary',
1068
+ 'sup',
1069
+ 'table',
1070
+ 'tbody',
1071
+ 'td',
1072
+ 'tfoot',
1073
+ 'th',
1074
+ 'thead',
1075
+ 'tr',
1076
+ 'u',
1077
+ 'ul',
1078
+ 'video'
1079
+ ];
1080
+ // A tag arrives one character at a time, so `<d`, `<di` and `<div` must all be
1081
+ // recognised or the raw text flashes for a chunk or two anyway. Every prefix of
1082
+ // every name above, precomputed — ~250 entries, one Set lookup per probe.
1083
+ const htmlPrefixes = new Set();
1084
+ for (const element of htmlElements) {
1085
+ for (let i = 1; i <= element.length; i++)
1086
+ htmlPrefixes.add(element.slice(0, i));
1087
+ }
1088
+ /** Could `name` still grow into a tag we are allowed to strip? */
1089
+ const isTagPrefix = (name, tags) => htmlPrefixes.has(name.toLowerCase()) ||
1090
+ // PascalCase is prefix-closed by construction, so this covers `<MyComp` too.
1091
+ tags.name.test(name) ||
1092
+ tags.names.some((tag) => tag.startsWith(name));
989
1093
  // Emphasis/code markers, as a set so inlineCitation can scan a cell for them
990
1094
  // without building a substring per bracket.
991
1095
  const formattingChars = new Set(['*', '~', '`', '_']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "packageManager": "pnpm@10.32.1",
5
5
  "repository": {
6
6
  "type": "git",