@sugarat/theme 0.5.11 → 0.5.12-beta.1

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 (49) hide show
  1. package/node.d.ts +2 -4
  2. package/node.js +206 -156
  3. package/node.mjs +205 -154
  4. package/package.json +10 -15
  5. package/src/components/Alert.vue +269 -0
  6. package/src/components/Avatar.vue +65 -0
  7. package/src/components/BlogAlert.vue +8 -8
  8. package/src/components/BlogApp.vue +5 -8
  9. package/src/components/BlogArticleAnalyze.vue +99 -66
  10. package/src/components/BlogAuthor.vue +13 -15
  11. package/src/components/BlogBackToTop.vue +21 -24
  12. package/src/components/BlogButtonAfterArticle.vue +12 -15
  13. package/src/components/BlogCommentWrapper.vue +34 -41
  14. package/src/components/BlogDocCover.vue +1 -1
  15. package/src/components/BlogFooter.vue +26 -32
  16. package/src/components/BlogFriendLink.vue +91 -73
  17. package/src/components/BlogHomeBanner.vue +25 -31
  18. package/src/components/BlogHomeHeaderAvatar.vue +16 -23
  19. package/src/components/BlogHomeInfo.vue +2 -4
  20. package/src/components/BlogHomeOverview.vue +12 -15
  21. package/src/components/BlogHomeTags.vue +22 -31
  22. package/src/components/BlogHotArticle.vue +69 -80
  23. package/src/components/BlogImagePreview.vue +3 -3
  24. package/src/components/BlogItem.vue +14 -23
  25. package/src/components/BlogList.vue +15 -19
  26. package/src/components/BlogRecommendArticle.vue +56 -72
  27. package/src/components/BlogSidebar.vue +1 -1
  28. package/src/components/Button.vue +150 -0
  29. package/src/components/Carousel.vue +249 -0
  30. package/src/components/CarouselItem.vue +139 -0
  31. package/src/components/CommentArtalk.vue +1 -1
  32. package/src/components/Image.vue +33 -0
  33. package/src/components/ImageViewer.vue +407 -0
  34. package/src/components/Pagination.vue +369 -0
  35. package/src/components/Tag.vue +144 -0
  36. package/src/components/UserWorks.vue +132 -175
  37. package/src/composables/config/blog.ts +6 -1
  38. package/src/composables/config/index.ts +2 -2
  39. package/src/index.ts +12 -19
  40. package/src/node.ts +0 -3
  41. package/src/styles/el-base.css +340 -0
  42. package/src/styles/{index.scss → index.css} +56 -91
  43. package/src/utils/client/index.ts +17 -0
  44. package/src/utils/node/mdPlugins.ts +1 -1
  45. package/src/utils/node/theme.ts +5 -2
  46. package/src/utils/node/vitePlugins.ts +51 -18
  47. package/src/styles/scss/algolia.scss +0 -231
  48. package/src/styles/scss/global.scss +0 -156
  49. package/src/styles/scss/highlight.scss +0 -12
package/node.d.ts CHANGED
@@ -4,7 +4,6 @@ import { Repo, Mapping } from '@giscus/vue';
4
4
  import { Options } from 'oh-my-live2d';
5
5
  import { PagefindConfig } from 'vitepress-plugin-pagefind';
6
6
  import { AnnouncementOptions } from 'vitepress-plugin-announcement';
7
- export { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
8
7
 
9
8
  type RSSPluginOptions = RSSOptions;
10
9
  type ThemeableImage = string | {
@@ -264,7 +263,7 @@ declare namespace Theme {
264
263
  tag?: string;
265
264
  }
266
265
  interface Alert {
267
- type: 'success' | 'warning' | 'info' | 'error';
266
+ type: 'success' | 'warning' | 'info' | 'error' | 'primary';
268
267
  /**
269
268
  * 细粒度的时间控制
270
269
  * 默认展示时间,-1 只展示1次,其它数字为每次都展示,一定时间后自动消失,0为不自动消失
@@ -397,7 +396,7 @@ declare namespace Theme {
397
396
  authorList?: Omit<FriendLink, 'avatar'>[];
398
397
  /**
399
398
  * 启用 [vitepress-plugin-tabs](https://www.npmjs.com/package/vitepress-plugin-tabs)
400
- * @default false
399
+ * @default true
401
400
  */
402
401
  tabs?: boolean;
403
402
  works?: UserWorks;
@@ -631,7 +630,6 @@ declare function getThemeConfig(cfg?: Partial<Theme.BlogConfig>): any;
631
630
  */
632
631
  declare function defineConfig(config: UserConfig<Theme.Config>): any;
633
632
  declare function defineLocaleConfig(cfg: Omit<Theme.BlogConfig, 'locales' | 'pagesData'>): Omit<Theme.BlogConfig, "locales" | "pagesData">;
634
-
635
633
  declare function footerHTML(footerData: Theme.FooterItem | Theme.FooterItem[]): string;
636
634
 
637
635
  export { defineConfig, defineLocaleConfig, footerHTML, getThemeConfig };
package/node.js CHANGED
@@ -33,176 +33,185 @@ __export(node_exports, {
33
33
  defineConfig: () => defineConfig,
34
34
  defineLocaleConfig: () => defineLocaleConfig,
35
35
  footerHTML: () => footerHTML,
36
- getThemeConfig: () => getThemeConfig,
37
- tabsMarkdownPlugin: () => tabsPlugin
36
+ getThemeConfig: () => getThemeConfig
38
37
  });
39
38
  module.exports = __toCommonJS(node_exports);
40
39
 
41
40
  // src/utils/node/mdPlugins.ts
42
41
  var import_module = require("module");
43
42
 
44
- // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@2.0.0-alpha.15_@types+node@24.5.2_async-validator_99dc32d207803fc3e59dad002dbf4f6a/node_modules/vitepress-plugin-tabs/dist/index.js
45
- var tabsMarker = "=tabs";
46
- var tabsMarkerLen = tabsMarker.length;
47
- var ruleBlockTabs = (state, startLine, endLine, silent) => {
48
- if (state.sCount[startLine] - state.blkIndent >= 4) {
49
- return false;
43
+ // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.7.3_vitepress@2.0.0-alpha.15_@types+node@24.5.2_async-validator_f00dcfd3cc5e72074cf41a606f1d799c/node_modules/vitepress-plugin-tabs/dist/node/index.js
44
+ function container_plugin(md, name, options) {
45
+ function validateDefault(params) {
46
+ return params.trim().split(" ", 2)[0] === name;
47
+ }
48
+ function renderDefault(tokens, idx, _options, env, slf) {
49
+ if (tokens[idx].nesting === 1)
50
+ tokens[idx].attrJoin("class", name);
51
+ return slf.renderToken(tokens, idx, _options, env, slf);
52
+ }
53
+ options = options || {};
54
+ const min_markers = 3;
55
+ const marker_str = options.marker || ":";
56
+ const marker_char = marker_str.charCodeAt(0);
57
+ const marker_len = marker_str.length;
58
+ const validate = options.validate || validateDefault;
59
+ const render = options.render || renderDefault;
60
+ function container(state, startLine, endLine, silent) {
61
+ let pos;
62
+ let auto_closed = false;
63
+ let start = state.bMarks[startLine] + state.tShift[startLine];
64
+ let max = state.eMarks[startLine];
65
+ if (marker_char !== state.src.charCodeAt(start))
66
+ return false;
67
+ for (pos = start + 1; pos <= max; pos++)
68
+ if (marker_str[(pos - start) % marker_len] !== state.src[pos])
69
+ break;
70
+ const marker_count = Math.floor((pos - start) / marker_len);
71
+ if (marker_count < min_markers)
72
+ return false;
73
+ pos -= (pos - start) % marker_len;
74
+ const markup = state.src.slice(start, pos);
75
+ const params = state.src.slice(pos, max);
76
+ if (!validate(params, markup))
77
+ return false;
78
+ if (silent)
79
+ return true;
80
+ let nextLine = startLine;
81
+ for (; ; ) {
82
+ nextLine++;
83
+ if (nextLine >= endLine)
84
+ break;
85
+ start = state.bMarks[nextLine] + state.tShift[nextLine];
86
+ max = state.eMarks[nextLine];
87
+ if (start < max && state.sCount[nextLine] < state.blkIndent)
88
+ break;
89
+ if (marker_char !== state.src.charCodeAt(start))
90
+ continue;
91
+ if (state.sCount[nextLine] - state.blkIndent >= 4)
92
+ continue;
93
+ for (pos = start + 1; pos <= max; pos++)
94
+ if (marker_str[(pos - start) % marker_len] !== state.src[pos])
95
+ break;
96
+ if (Math.floor((pos - start) / marker_len) < marker_count)
97
+ continue;
98
+ pos -= (pos - start) % marker_len;
99
+ pos = state.skipSpaces(pos);
100
+ if (pos < max)
101
+ continue;
102
+ auto_closed = true;
103
+ break;
104
+ }
105
+ const old_parent = state.parentType;
106
+ const old_line_max = state.lineMax;
107
+ state.parentType = "container";
108
+ state.lineMax = nextLine;
109
+ const token_o = state.push("container_" + name + "_open", "div", 1);
110
+ token_o.markup = markup;
111
+ token_o.block = true;
112
+ token_o.info = params;
113
+ token_o.map = [startLine, nextLine];
114
+ state.md.block.tokenize(state, startLine + 1, nextLine);
115
+ const token_c = state.push("container_" + name + "_close", "div", -1);
116
+ token_c.markup = state.src.slice(start, pos);
117
+ token_c.block = true;
118
+ state.parentType = old_parent;
119
+ state.lineMax = old_line_max;
120
+ state.line = nextLine + (auto_closed ? 1 : 0);
121
+ return true;
50
122
  }
123
+ md.block.ruler.before("fence", "container_" + name, container, { alt: [
124
+ "paragraph",
125
+ "reference",
126
+ "blockquote",
127
+ "list"
128
+ ] });
129
+ md.renderer.rules["container_" + name + "_open"] = render;
130
+ md.renderer.rules["container_" + name + "_close"] = render;
131
+ }
132
+ var tabMarkerCode = "=".charCodeAt(0);
133
+ var minTabMarkerLen = 2;
134
+ var ruleBlockTab = (state, startLine, endLine, silent) => {
51
135
  let pos = state.bMarks[startLine] + state.tShift[startLine];
52
- let max = state.eMarks[startLine];
53
- if (pos + 3 > max) {
136
+ const max = state.eMarks[startLine];
137
+ if (state.parentType !== "container")
138
+ return false;
139
+ if (pos + minTabMarkerLen > max)
54
140
  return false;
55
- }
56
141
  const marker = state.src.charCodeAt(pos);
57
- if (marker !== 58) {
142
+ if (marker !== tabMarkerCode)
58
143
  return false;
59
- }
60
144
  const mem = pos;
61
- pos = state.skipChars(pos, marker);
62
- let len = pos - mem;
63
- if (len < 3) {
64
- return false;
65
- }
66
- if (state.src.slice(pos, pos + tabsMarkerLen) !== tabsMarker) {
145
+ pos = state.skipChars(pos + 1, marker);
146
+ const tabMarkerLen = pos - mem;
147
+ if (tabMarkerLen < minTabMarkerLen - 1)
67
148
  return false;
68
- }
69
- pos += tabsMarkerLen;
70
- if (silent) {
149
+ if (silent)
71
150
  return true;
72
- }
73
- const markup = state.src.slice(mem, pos);
74
- const params = state.src.slice(pos, max);
75
151
  let nextLine = startLine;
76
- let haveEndMarker = false;
152
+ let endStart = mem;
153
+ let endPos = pos;
77
154
  for (; ; ) {
78
155
  nextLine++;
79
- if (nextLine >= endLine) {
156
+ if (nextLine >= endLine)
80
157
  break;
81
- }
82
- pos = state.bMarks[nextLine] + state.tShift[nextLine];
83
- const mem2 = pos;
84
- max = state.eMarks[nextLine];
85
- if (pos < max && state.sCount[nextLine] < state.blkIndent) {
158
+ endStart = state.bMarks[nextLine] + state.tShift[nextLine];
159
+ const max$1 = state.eMarks[nextLine];
160
+ if (endStart < max$1 && state.sCount[nextLine] < state.blkIndent)
86
161
  break;
87
- }
88
- if (state.src.charCodeAt(pos) !== marker) {
89
- continue;
90
- }
91
- if (state.sCount[nextLine] - state.blkIndent >= 4) {
162
+ if (state.src.charCodeAt(endStart) !== tabMarkerCode)
92
163
  continue;
93
- }
94
- pos = state.skipChars(pos, marker);
95
- if (pos - mem2 < len) {
164
+ const p = state.skipChars(endStart + 1, marker);
165
+ if (p - endStart !== tabMarkerLen)
96
166
  continue;
97
- }
98
- pos = state.skipSpaces(pos);
99
- if (pos < max) {
100
- continue;
101
- }
102
- haveEndMarker = true;
167
+ endPos = p;
103
168
  break;
104
169
  }
105
- len = state.sCount[startLine];
106
- state.line = nextLine + (haveEndMarker ? 1 : 0);
107
- const token = state.push("tabs", "div", 0);
108
- token.info = params;
109
- token.content = state.getLines(startLine + 1, nextLine, len, true);
110
- token.markup = markup;
111
- token.map = [startLine, state.line];
170
+ const oldParent = state.parentType;
171
+ const oldLineMax = state.lineMax;
172
+ state.parentType = "tab";
173
+ state.lineMax = nextLine;
174
+ const startToken = state.push("tab_open", "div", 1);
175
+ startToken.markup = state.src.slice(mem, pos);
176
+ startToken.block = true;
177
+ startToken.info = state.src.slice(pos, max).trimStart();
178
+ startToken.map = [startLine, nextLine - 1];
179
+ state.md.block.tokenize(state, startLine + 1, nextLine);
180
+ const endToken = state.push("tab_close", "div", -1);
181
+ endToken.markup = state.src.slice(endStart, endPos);
182
+ endToken.block = true;
183
+ state.parentType = oldParent;
184
+ state.lineMax = oldLineMax;
185
+ state.line = nextLine;
112
186
  return true;
113
187
  };
114
- var tabBreakRE = /^\s*::(.+)$/;
115
- var forbiddenCharsInSlotNames = /[ '"]/;
116
- var parseTabBreakLine = (line) => {
117
- const m = line.match(tabBreakRE);
118
- if (!m)
119
- return null;
120
- const trimmed = m[1].trim();
121
- if (forbiddenCharsInSlotNames.test(trimmed)) {
122
- throw new Error(
123
- `contains forbidden chars in slot names (space and quotes) (${JSON.stringify(
124
- line
125
- )})`
126
- );
127
- }
128
- return trimmed;
129
- };
130
- var lastLineBreakRE = /\n$/;
131
- var parseTabsContent = (content) => {
132
- const lines = content.replace(lastLineBreakRE, "").split("\n");
133
- const tabInfos = [];
134
- const tabLabels = /* @__PURE__ */ new Set();
135
- let currentTab = null;
136
- const createTabInfo = (label) => {
137
- if (tabLabels.has(label)) {
138
- throw new Error(`a tab labelled ${JSON.stringify(label)} already exists`);
139
- }
140
- const newTab = { label, content: [] };
141
- tabInfos.push(newTab);
142
- tabLabels.add(label);
143
- return newTab;
144
- };
145
- for (const line of lines) {
146
- const tabLabel = parseTabBreakLine(line);
147
- if (currentTab === null) {
148
- if (tabLabel === null) {
149
- throw new Error(
150
- `tabs should start with \`::\${tabLabel}\` (e.g. "::foo"). (received: ${JSON.stringify(
151
- line
152
- )})`
153
- );
154
- }
155
- currentTab = createTabInfo(tabLabel);
156
- continue;
157
- }
158
- if (tabLabel === null) {
159
- currentTab.content.push(line);
160
- } else {
161
- currentTab = createTabInfo(tabLabel);
162
- }
163
- }
164
- if (tabInfos.length < 0) {
165
- throw new Error("tabs should include at least one tab");
166
- }
167
- return tabInfos.map((info) => ({
168
- label: info.label,
169
- content: info.content.join("\n").replace(lastLineBreakRE, "")
170
- }));
171
- };
172
- var parseParams = (input) => {
173
- if (!input.startsWith("=")) {
174
- return {
175
- shareStateKey: void 0
176
- };
177
- }
178
- const splitted = input.split("=");
179
- return {
180
- shareStateKey: splitted[1]
181
- };
188
+ var parseTabsParams = (input) => {
189
+ return { shareStateKey: input.match(/key:(\S+)/)?.[1] };
182
190
  };
183
191
  var tabsPlugin = (md) => {
184
- md.block.ruler.before("fence", "=tabs", ruleBlockTabs, {
185
- alt: ["paragraph", "reference", "blockquote", "list"]
186
- });
187
- md.renderer.rules.tabs = (tokens, index, _options, env) => {
192
+ md.use(container_plugin, "tabs", { render(tokens, index) {
188
193
  const token = tokens[index];
189
- const tabs = parseTabsContent(token.content);
190
- const renderedTabs = tabs.map((tab) => ({
191
- label: tab.label,
192
- content: md.render(tab.content, env)
193
- }));
194
- const params = parseParams(token.info);
195
- const tabLabelsProp = `:tabLabels="${md.utils.escapeHtml(
196
- JSON.stringify(tabs.map((tab) => tab.label))
197
- )}"`;
198
- const shareStateKeyProp = params.shareStateKey ? `sharedStateKey="${md.utils.escapeHtml(params.shareStateKey)}"` : "";
199
- const slots = renderedTabs.map(
200
- (tab) => `<template #${tab.label}>${tab.content}</template>`
201
- );
202
- return `<PluginTabs ${tabLabelsProp} ${shareStateKeyProp}>${slots.join(
203
- ""
204
- )}</PluginTabs>`;
194
+ if (token.nesting === 1) {
195
+ const params = parseTabsParams(token.info);
196
+ return `<PluginTabs ${params.shareStateKey ? `sharedStateKey="${md.utils.escapeHtml(params.shareStateKey)}"` : ""}>
197
+ `;
198
+ } else
199
+ return `</PluginTabs>
200
+ `;
201
+ } });
202
+ md.block.ruler.after("container_tabs", "tab", ruleBlockTab);
203
+ const renderTab = (tokens, index) => {
204
+ const token = tokens[index];
205
+ if (token.nesting === 1) {
206
+ const label = token.info;
207
+ return `<PluginTabsTab ${`label="${md.utils.escapeHtml(label)}"`}>
208
+ `;
209
+ } else
210
+ return `</PluginTabsTab>
211
+ `;
205
212
  };
213
+ md.renderer.rules["tab_open"] = renderTab;
214
+ md.renderer.rules["tab_close"] = renderTab;
206
215
  };
207
216
 
208
217
  // src/utils/node/mdPlugins.ts
@@ -329,7 +338,6 @@ function patchOptimizeDeps(config) {
329
338
  config.vite.optimizeDeps = {};
330
339
  }
331
340
  config.vite.optimizeDeps.exclude = ["vitepress-plugin-tabs", "@sugarat/theme"];
332
- config.vite.optimizeDeps.include = ["element-plus"];
333
341
  }
334
342
 
335
343
  // src/utils/node/theme.ts
@@ -338,6 +346,14 @@ var import_node_path2 = __toESM(require("path"));
338
346
  var import_theme_shared2 = require("@sugarat/theme-shared");
339
347
 
340
348
  // src/utils/client/index.ts
349
+ function shuffleArray(arr) {
350
+ const array = [...arr];
351
+ for (let i = array.length - 1; i > 0; i--) {
352
+ const j = Math.floor(Math.random() * (i + 1));
353
+ [array[i], array[j]] = [array[j], array[i]];
354
+ }
355
+ return array;
356
+ }
341
357
  function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
342
358
  if (!(d instanceof Date)) {
343
359
  d = new Date(d);
@@ -459,6 +475,10 @@ function patchVPThemeConfig(cfg, vpThemeConfig = {}) {
459
475
  return vpThemeConfig;
460
476
  }
461
477
  function checkConfig(cfg) {
478
+ const friendConfig = cfg?.friend;
479
+ if (!Array.isArray(friendConfig) && friendConfig?.random) {
480
+ friendConfig.list = shuffleArray(friendConfig.list);
481
+ }
462
482
  }
463
483
 
464
484
  // src/utils/node/vitePlugins.ts
@@ -579,20 +599,30 @@ function getVitePlugins(cfg = {}) {
579
599
  plugins.push(patchGroupIconPlugin());
580
600
  plugins.push((0, import_vitepress_plugin_group_icons2.groupIconVitePlugin)(cfg?.groupIcon));
581
601
  }
602
+ if (cfg?.tabs !== false) {
603
+ plugins.push(patchTabsPlugin());
604
+ }
605
+ if (cfg?.timeline !== false) {
606
+ plugins.push(patchTimelinePlugin());
607
+ }
582
608
  return plugins;
583
609
  }
584
610
  function patchGroupIconPlugin() {
585
- return {
611
+ return createPatchPlugin({
586
612
  name: "@sugarat/theme-plugin-patch-group-icon",
587
- enforce: "pre",
588
- transform(code, id) {
589
- if (id.match(/[\/\\]theme[\/\\]index\.(ts|js)$/)) {
590
- return `import 'virtual:group-icons.css'
591
- ${code}`;
592
- }
593
- return code;
613
+ replacements: {
614
+ "// replace-group-icon-import-code": "import 'virtual:group-icons.css'"
594
615
  }
595
- };
616
+ });
617
+ }
618
+ function patchTabsPlugin() {
619
+ return createPatchPlugin({
620
+ name: "@sugarat/theme-plugin-patch-tabs",
621
+ replacements: {
622
+ "// replace-tabs-import-code": "import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'",
623
+ "// replace-tabs-enhance-app-code": "enhanceAppWithTabs(ctx.app)"
624
+ }
625
+ });
596
626
  }
597
627
  function registerVitePlugins(vpCfg, plugins) {
598
628
  vpCfg.vite = {
@@ -601,12 +631,25 @@ function registerVitePlugins(vpCfg, plugins) {
601
631
  };
602
632
  }
603
633
  function inlineInjectMermaidClient() {
604
- return {
634
+ return createPatchPlugin({
605
635
  name: "@sugarat/theme-plugin-inline-inject-mermaid-client",
636
+ replacements: {
637
+ "// replace-mermaid-import-code": "import Mermaid from 'vitepress-plugin-mermaid/Mermaid.vue'",
638
+ "// replace-mermaid-mounted-code": "if (!ctx.app.component('Mermaid')) { ctx.app.component('Mermaid', Mermaid as any) }"
639
+ }
640
+ });
641
+ }
642
+ function createPatchPlugin({ name, replacements }) {
643
+ return {
644
+ name,
606
645
  enforce: "pre",
607
646
  transform(code, id) {
608
- if (id.endsWith("src/index.ts") && code.startsWith("// @sugarat/theme index")) {
609
- return code.replace("// replace-mermaid-import-code", "import Mermaid from 'vitepress-plugin-mermaid/Mermaid.vue'").replace("// replace-mermaid-mounted-code", "if (!ctx.app.component('Mermaid')) { ctx.app.component('Mermaid', Mermaid as any) }");
647
+ if (id.endsWith("theme/src/index.ts") && code.startsWith("// @sugarat/theme index")) {
648
+ let newCode = code;
649
+ for (const [key, value] of Object.entries(replacements)) {
650
+ newCode = newCode.replace(key, value);
651
+ }
652
+ return newCode;
610
653
  }
611
654
  return code;
612
655
  }
@@ -769,6 +812,14 @@ function setThemeScript(themeColor) {
769
812
  };
770
813
  return pluginOps;
771
814
  }
815
+ function patchTimelinePlugin() {
816
+ return createPatchPlugin({
817
+ name: "@sugarat/theme-plugin-patch-timeline",
818
+ replacements: {
819
+ "// replace-timeline-import-code": "import 'vitepress-markdown-timeline/dist/theme/index.css'"
820
+ }
821
+ });
822
+ }
772
823
 
773
824
  // src/node.ts
774
825
  function getThemeConfig(cfg = {}) {
@@ -834,6 +885,5 @@ function footerHTML(footerData) {
834
885
  defineConfig,
835
886
  defineLocaleConfig,
836
887
  footerHTML,
837
- getThemeConfig,
838
- tabsMarkdownPlugin
888
+ getThemeConfig
839
889
  });