@opendocsdev/cli 0.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 (78) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +300 -0
  3. package/dist/bin/opendocs.js +712 -0
  4. package/dist/bin/opendocs.js.map +1 -0
  5. package/dist/templates/api-reference.mdx +308 -0
  6. package/dist/templates/components.mdx +286 -0
  7. package/dist/templates/configuration.mdx +190 -0
  8. package/dist/templates/docs.json +27 -0
  9. package/dist/templates/introduction.mdx +25 -0
  10. package/dist/templates/logo.svg +4 -0
  11. package/dist/templates/quickstart.mdx +59 -0
  12. package/dist/templates/writing-content.mdx +236 -0
  13. package/package.json +92 -0
  14. package/src/engine/astro.config.ts +75 -0
  15. package/src/engine/src/components/Analytics.astro +57 -0
  16. package/src/engine/src/components/ApiPlayground.astro +24 -0
  17. package/src/engine/src/components/Callout.astro +66 -0
  18. package/src/engine/src/components/Card.astro +75 -0
  19. package/src/engine/src/components/CardGroup.astro +29 -0
  20. package/src/engine/src/components/CodeGroup.astro +231 -0
  21. package/src/engine/src/components/CopyButton.astro +179 -0
  22. package/src/engine/src/components/Steps.astro +27 -0
  23. package/src/engine/src/components/Tab.astro +21 -0
  24. package/src/engine/src/components/TableOfContents.astro +119 -0
  25. package/src/engine/src/components/Tabs.astro +135 -0
  26. package/src/engine/src/components/index.ts +107 -0
  27. package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
  28. package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
  29. package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
  30. package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
  31. package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
  32. package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
  33. package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
  34. package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
  35. package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
  36. package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
  37. package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
  38. package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
  39. package/src/engine/src/components/react/Callout.tsx +54 -0
  40. package/src/engine/src/components/react/Card.tsx +48 -0
  41. package/src/engine/src/components/react/CardGroup.tsx +24 -0
  42. package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
  43. package/src/engine/src/components/react/GitHubLink.tsx +28 -0
  44. package/src/engine/src/components/react/NavigationCard.tsx +53 -0
  45. package/src/engine/src/components/react/PageActions.tsx +124 -0
  46. package/src/engine/src/components/react/PageFooter.tsx +91 -0
  47. package/src/engine/src/components/react/SearchModal.tsx +358 -0
  48. package/src/engine/src/components/react/SearchProvider.tsx +37 -0
  49. package/src/engine/src/components/react/Sidebar.tsx +369 -0
  50. package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
  51. package/src/engine/src/components/react/Steps.tsx +25 -0
  52. package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
  53. package/src/engine/src/components/react/index.ts +14 -0
  54. package/src/engine/src/env.d.ts +10 -0
  55. package/src/engine/src/layouts/DocsLayout.astro +357 -0
  56. package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
  57. package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
  58. package/src/engine/src/lib/config.ts +79 -0
  59. package/src/engine/src/lib/markdown.ts +54 -0
  60. package/src/engine/src/lib/mdx-loader.ts +143 -0
  61. package/src/engine/src/lib/mdx-utils.ts +72 -0
  62. package/src/engine/src/lib/remark-opendocs.ts +195 -0
  63. package/src/engine/src/lib/utils.ts +221 -0
  64. package/src/engine/src/pages/[...slug].astro +115 -0
  65. package/src/engine/src/pages/index.astro +71 -0
  66. package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
  67. package/src/engine/src/scripts/theme-init.ts +25 -0
  68. package/src/engine/src/styles/global.css +703 -0
  69. package/src/engine/tailwind.config.mjs +60 -0
  70. package/src/engine/tsconfig.json +15 -0
  71. package/src/templates/api-reference.mdx +308 -0
  72. package/src/templates/components.mdx +286 -0
  73. package/src/templates/configuration.mdx +190 -0
  74. package/src/templates/docs.json +27 -0
  75. package/src/templates/introduction.mdx +25 -0
  76. package/src/templates/logo.svg +4 -0
  77. package/src/templates/quickstart.mdx +59 -0
  78. package/src/templates/writing-content.mdx +236 -0
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { DocsConfig } from "../../../config/schema.js";
4
+
5
+ /**
6
+ * Get the project directory from environment variable or fallback to cwd
7
+ */
8
+ export function getProjectDir(): string {
9
+ return process.env.OPENDOCS_PROJECT_DIR || process.cwd();
10
+ }
11
+
12
+ // Cache for loaded configuration to avoid repeated file reads
13
+ let cachedConfig: DocsConfig | null = null;
14
+ let cachedConfigPath: string | null = null;
15
+
16
+ /**
17
+ * Load and parse the docs.json configuration file from the project directory.
18
+ * Results are cached to prevent re-reading on every component render.
19
+ * Caching is disabled in development mode to allow hot-reloading of config changes.
20
+ */
21
+ export async function loadConfig(): Promise<DocsConfig> {
22
+ const projectDir = getProjectDir();
23
+ const configPath = path.join(projectDir, "docs.json");
24
+ const isDev = process.env.NODE_ENV !== "production";
25
+
26
+ // Return cached config if available, path hasn't changed, and not in dev mode
27
+ if (cachedConfig && cachedConfigPath === configPath && !isDev) {
28
+ return cachedConfig;
29
+ }
30
+
31
+ try {
32
+ const configContent = await fs.readFile(configPath, "utf-8");
33
+ cachedConfig = JSON.parse(configContent) as DocsConfig;
34
+ cachedConfigPath = configPath;
35
+ return cachedConfig;
36
+ } catch {
37
+ // Return minimal default config if docs.json is not found
38
+ const defaultConfig: DocsConfig = {
39
+ name: "Documentation",
40
+ navigation: [],
41
+ snippets: ["snippets"],
42
+ };
43
+ cachedConfig = defaultConfig;
44
+ cachedConfigPath = configPath;
45
+ return defaultConfig;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Clear the config cache. Useful for development/hot-reload scenarios.
51
+ */
52
+ export function clearConfigCache(): void {
53
+ cachedConfig = null;
54
+ cachedConfigPath = null;
55
+ }
56
+
57
+ /**
58
+ * Get the path to the user's content directory
59
+ */
60
+ export function getContentDir(): string {
61
+ return getProjectDir();
62
+ }
63
+
64
+ /**
65
+ * Get the path to the user's public assets directory
66
+ */
67
+ export function getPublicDir(): string {
68
+ return path.join(getProjectDir(), "public");
69
+ }
70
+
71
+ /**
72
+ * Get the path to an MDX file in the user's project
73
+ */
74
+ export function getMdxPath(pagePath: string): string {
75
+ const projectDir = getProjectDir();
76
+ // Add .mdx extension if not present
77
+ const fileName = pagePath.endsWith(".mdx") ? pagePath : `${pagePath}.mdx`;
78
+ return path.join(projectDir, fileName);
79
+ }
@@ -0,0 +1,54 @@
1
+ export interface Frontmatter {
2
+ title?: string;
3
+ description?: string;
4
+ [key: string]: string | undefined;
5
+ }
6
+
7
+ export interface Heading {
8
+ depth: number;
9
+ slug: string;
10
+ text: string;
11
+ }
12
+
13
+ /**
14
+ * Parse frontmatter from markdown/MDX content
15
+ */
16
+ export function parseFrontmatter(content: string): {
17
+ frontmatter: Frontmatter;
18
+ body: string;
19
+ } {
20
+ // Match frontmatter: starts with ---, optional content, ends with ---
21
+ const match = content.match(/^---[\r\n]+([\s\S]*?)[\r\n]*---[\r\n]*/);
22
+ if (!match) return { frontmatter: {}, body: content };
23
+
24
+ const fm: Frontmatter = {};
25
+ const frontmatterContent = match[1].trim();
26
+ if (frontmatterContent) {
27
+ for (const line of frontmatterContent.split("\n")) {
28
+ const m = line.match(/^(\w+):\s*["']?(.+?)["']?\s*$/);
29
+ if (m) fm[m[1]] = m[2];
30
+ }
31
+ }
32
+ return { frontmatter: fm, body: content.slice(match[0].length).trim() };
33
+ }
34
+
35
+ /**
36
+ * Extract headings from markdown content for table of contents
37
+ */
38
+ export function extractHeadings(content: string): Heading[] {
39
+ const headings: Heading[] = [];
40
+ const regex = /^(#{2,6})\s+(.+)$/gm;
41
+ let m;
42
+ while ((m = regex.exec(content)) !== null) {
43
+ const text = m[2].trim();
44
+ headings.push({
45
+ depth: m[1].length,
46
+ text,
47
+ slug: text
48
+ .toLowerCase()
49
+ .replace(/[^a-z0-9]+/g, "-")
50
+ .replace(/(^-|-$)/g, ""),
51
+ });
52
+ }
53
+ return headings;
54
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * MDX Loader - Dynamically imports MDX files from user's content directory
3
+ * Uses the @content Vite alias which points to OPENDOCS_PROJECT_DIR
4
+ */
5
+
6
+ import type { AstroComponentFactory } from "astro/runtime/server/index.js";
7
+ import {
8
+ isSnippetPath as isSnippetPathUtil,
9
+ extractSlugFromPath as extractSlugFromPathUtil,
10
+ filterPathsToSlugs as filterPathsToSlugsUtil,
11
+ } from "./mdx-utils.js";
12
+
13
+ export interface Frontmatter {
14
+ title?: string;
15
+ description?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ /**
20
+ * Props for the MDX Content component
21
+ * The components prop allows injecting custom components for MDX rendering
22
+ */
23
+ export interface ContentProps {
24
+ components?: Record<string, AstroComponentFactory>;
25
+ }
26
+
27
+ export interface MDXModule {
28
+ default: AstroComponentFactory;
29
+ frontmatter?: Frontmatter;
30
+ }
31
+
32
+ export interface LoadedMDX {
33
+ Content: AstroComponentFactory;
34
+ frontmatter: Frontmatter;
35
+ }
36
+
37
+ // Get configured snippet folders from environment variable (set by dev.ts/build.ts)
38
+ // Default to ["snippets"] if not configured
39
+ function getSnippetFolders(): string[] {
40
+ const snippetsEnv = import.meta.env.OPENDOCS_SNIPPETS;
41
+ if (snippetsEnv) {
42
+ try {
43
+ return JSON.parse(snippetsEnv);
44
+ } catch {
45
+ // Invalid JSON in env var, fall back to default
46
+ return ["snippets"];
47
+ }
48
+ }
49
+ return ["snippets"];
50
+ }
51
+
52
+ const snippetFolders = getSnippetFolders();
53
+
54
+ // Get all MDX files from user's content directory
55
+ // This glob pattern is resolved at build time via the @content Vite alias
56
+ const mdxModules = import.meta.glob<MDXModule>("@content/**/*.mdx");
57
+ const mdModules = import.meta.glob<MDXModule>("@content/**/*.md");
58
+
59
+ /**
60
+ * Check if a path is in a configured snippet folder
61
+ * Uses default snippet folders from environment
62
+ */
63
+ function isSnippetPath(path: string): boolean {
64
+ return isSnippetPathUtil(path, snippetFolders);
65
+ }
66
+
67
+ /**
68
+ * Extract slug from a glob path
69
+ * Handles both @content/... and relative paths like ../../../../test-docs/...
70
+ * Returns null for paths in configured snippet folders (they should not become pages)
71
+ * Uses default snippet folders from environment
72
+ */
73
+ function extractSlugFromPath(globPath: string): string | null {
74
+ return extractSlugFromPathUtil(globPath, snippetFolders);
75
+ }
76
+
77
+ /**
78
+ * Build a map of slug -> loader function
79
+ * Excludes files in configured snippet folders
80
+ */
81
+ function buildSlugMap(): Map<string, () => Promise<MDXModule>> {
82
+ const slugMap = new Map<string, () => Promise<MDXModule>>();
83
+
84
+ for (const [path, loader] of Object.entries(mdxModules)) {
85
+ const slug = extractSlugFromPath(path);
86
+ // Skip snippet folder paths (extractSlugFromPath returns null for these)
87
+ if (slug !== null) {
88
+ slugMap.set(slug, loader);
89
+ }
90
+ }
91
+
92
+ for (const [path, loader] of Object.entries(mdModules)) {
93
+ const slug = extractSlugFromPath(path);
94
+ // Skip snippet folder paths and don't override .mdx with .md
95
+ if (slug !== null && !slugMap.has(slug)) {
96
+ slugMap.set(slug, loader);
97
+ }
98
+ }
99
+
100
+ return slugMap;
101
+ }
102
+
103
+ // Build the slug map once at module load time
104
+ const slugMap = buildSlugMap();
105
+
106
+ /**
107
+ * Load an MDX file by slug (e.g., "introduction" or "api/reference")
108
+ * Returns the Content component and frontmatter
109
+ */
110
+ export async function loadMDX(slug: string): Promise<LoadedMDX | null> {
111
+ const loader = slugMap.get(slug);
112
+
113
+ if (!loader) {
114
+ console.error(`MDX file not found for slug: ${slug}`);
115
+ console.error("Available slugs:", Array.from(slugMap.keys()));
116
+ return null;
117
+ }
118
+
119
+ try {
120
+ const module = await loader();
121
+ return {
122
+ Content: module.default,
123
+ frontmatter: module.frontmatter || {},
124
+ };
125
+ } catch (error) {
126
+ console.error(`Error loading MDX for slug ${slug}:`, error);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get all available content slugs (for static path generation)
133
+ */
134
+ export function getAvailableSlugs(): string[] {
135
+ return Array.from(slugMap.keys());
136
+ }
137
+
138
+ // Re-export pure utility functions for testing purposes
139
+ export {
140
+ isSnippetPath as isSnippetPathUtil,
141
+ extractSlugFromPath as extractSlugFromPathUtil,
142
+ filterPathsToSlugs as filterPathsToSlugsUtil,
143
+ } from "./mdx-utils.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * MDX Utility Functions - Pure functions for path and slug manipulation
3
+ * Extracted from mdx-loader.ts for testability
4
+ */
5
+
6
+ /**
7
+ * Check if a path is in a configured snippet folder
8
+ */
9
+ export function isSnippetPath(path: string, folders: string[]): boolean {
10
+ return folders.some(
11
+ (folder) => path.includes(`/${folder}/`) || path.startsWith(`${folder}/`)
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Extract slug from a glob path
17
+ * Handles both @content/... and relative paths like ../../../../test-docs/...
18
+ * Returns null for paths in configured snippet folders (they should not become pages)
19
+ */
20
+ export function extractSlugFromPath(
21
+ globPath: string,
22
+ folders: string[]
23
+ ): string | null {
24
+ // Remove @content/ prefix if present
25
+ if (globPath.startsWith("@content/")) {
26
+ const relativePath = globPath.replace(/^@content\//, "");
27
+ // Check if this is a snippet folder path
28
+ if (isSnippetPath(relativePath, folders)) {
29
+ return null;
30
+ }
31
+ return relativePath.replace(/\.mdx?$/, "");
32
+ }
33
+
34
+ // Handle relative paths - check if in snippet folder
35
+ if (isSnippetPath(globPath, folders)) {
36
+ return null;
37
+ }
38
+
39
+ // Handle relative paths from Vite glob
40
+ // e.g., "../../../../test-docs/guides/using-snippets.mdx" -> "guides/using-snippets"
41
+ // e.g., "../../../../test-docs/components.mdx" -> "components"
42
+ // Find the project directory boundary (last ../ followed by project-dir/)
43
+ // The pattern matches all the "../" parts, then project-dir/, then captures the slug
44
+ const relativeMatch = globPath.match(/(?:\.\.\/)+[^/]+\/(.+)\.mdx?$/);
45
+ if (relativeMatch) {
46
+ return relativeMatch[1];
47
+ }
48
+
49
+ // Fallback: just extract filename
50
+ const filenameMatch = globPath.match(/\/([^/]+)\.mdx?$/);
51
+ if (filenameMatch) {
52
+ return filenameMatch[1];
53
+ }
54
+
55
+ // Fallback: just remove extension
56
+ return globPath.replace(/\.mdx?$/, "");
57
+ }
58
+
59
+ /**
60
+ * Filter paths and return valid slugs (excludes snippet folders)
61
+ * Used for testing purposes
62
+ */
63
+ export function filterPathsToSlugs(paths: string[], folders: string[]): string[] {
64
+ const slugs: string[] = [];
65
+ for (const path of paths) {
66
+ const slug = extractSlugFromPath(path, folders);
67
+ if (slug !== null && !slugs.includes(slug)) {
68
+ slugs.push(slug);
69
+ }
70
+ }
71
+ return slugs;
72
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Unified remark plugin for opendocs MDX transformations.
3
+ * Handles all custom AST modifications in a single pass.
4
+ */
5
+ import { visit } from "unist-util-visit";
6
+ import type { Root } from "mdast";
7
+
8
+ interface MdxJsxAttribute {
9
+ type: "mdxJsxAttribute";
10
+ name: string;
11
+ value: string | { type: string; value: string } | null;
12
+ }
13
+
14
+ interface MdxJsxFlowElement {
15
+ type: "mdxJsxFlowElement";
16
+ name: string | null;
17
+ attributes: MdxJsxAttribute[];
18
+ children: unknown[];
19
+ }
20
+
21
+ interface CodeNode {
22
+ type: "code";
23
+ lang?: string | null;
24
+ meta?: string | null;
25
+ value: string;
26
+ }
27
+
28
+ // ============================================
29
+ // Type Guards
30
+ // ============================================
31
+
32
+ function isMdxJsxFlowElement(node: unknown): node is MdxJsxFlowElement {
33
+ return (
34
+ typeof node === "object" &&
35
+ node !== null &&
36
+ (node as { type?: string }).type === "mdxJsxFlowElement"
37
+ );
38
+ }
39
+
40
+ function isCodeNode(node: unknown): node is CodeNode {
41
+ return (
42
+ typeof node === "object" &&
43
+ node !== null &&
44
+ (node as { type?: string }).type === "code"
45
+ );
46
+ }
47
+
48
+ // ============================================
49
+ // CodeGroup: Extract code block labels
50
+ // ============================================
51
+
52
+ function extractCodeLabels(children: unknown[]): string[] {
53
+ const labels: string[] = [];
54
+
55
+ function traverse(nodes: unknown[]) {
56
+ for (const node of nodes) {
57
+ if (isCodeNode(node)) {
58
+ const label = node.meta || node.lang || "code";
59
+ labels.push(label);
60
+ } else if (
61
+ typeof node === "object" &&
62
+ node !== null &&
63
+ Array.isArray((node as { children?: unknown[] }).children)
64
+ ) {
65
+ traverse((node as { children: unknown[] }).children);
66
+ }
67
+ }
68
+ }
69
+
70
+ traverse(children);
71
+ return labels;
72
+ }
73
+
74
+ function processCodeGroup(node: MdxJsxFlowElement) {
75
+ const labels = extractCodeLabels(node.children);
76
+ if (labels.length === 0) return;
77
+
78
+ node.attributes = node.attributes || [];
79
+ node.attributes.push({
80
+ type: "mdxJsxAttribute",
81
+ name: "data-labels",
82
+ value: JSON.stringify(labels),
83
+ });
84
+ node.attributes.push({
85
+ type: "mdxJsxAttribute",
86
+ name: "data-sync-key",
87
+ value: labels.slice().sort().join(":"),
88
+ });
89
+ }
90
+
91
+ // ============================================
92
+ // Tabs: Extract tab labels and icons
93
+ // ============================================
94
+
95
+ interface TabInfo {
96
+ label: string;
97
+ icon?: string;
98
+ }
99
+
100
+ function getAttributeValue(
101
+ attributes: MdxJsxAttribute[],
102
+ name: string
103
+ ): string | undefined {
104
+ const attr = attributes.find((a) => a.name === name);
105
+ if (!attr) return undefined;
106
+ if (typeof attr.value === "string") return attr.value;
107
+ if (attr.value && typeof attr.value === "object" && "value" in attr.value) {
108
+ return attr.value.value;
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ function extractTabsInfo(children: unknown[]): TabInfo[] {
114
+ const tabs: TabInfo[] = [];
115
+ for (const child of children) {
116
+ if (isMdxJsxFlowElement(child) && child.name === "Tab") {
117
+ const label = getAttributeValue(child.attributes, "label");
118
+ const icon = getAttributeValue(child.attributes, "icon");
119
+ if (label) tabs.push({ label, icon });
120
+ }
121
+ }
122
+ return tabs;
123
+ }
124
+
125
+ function processTabs(node: MdxJsxFlowElement) {
126
+ const tabsInfo = extractTabsInfo(node.children);
127
+ if (tabsInfo.length === 0) return;
128
+
129
+ node.attributes = node.attributes || [];
130
+ node.attributes.push({
131
+ type: "mdxJsxAttribute",
132
+ name: "data-tabs",
133
+ value: JSON.stringify(tabsInfo),
134
+ });
135
+ }
136
+
137
+ // ============================================
138
+ // React Hooks: Auto-inject imports
139
+ // ============================================
140
+
141
+ const REACT_HOOKS_IMPORT = {
142
+ type: "mdxjsEsm",
143
+ value: `import { useState, useEffect, useMemo, useCallback, useRef, useContext, useReducer, Fragment } from 'react';`,
144
+ data: {
145
+ estree: {
146
+ type: "Program",
147
+ sourceType: "module",
148
+ body: [
149
+ {
150
+ type: "ImportDeclaration",
151
+ specifiers: [
152
+ "useState",
153
+ "useEffect",
154
+ "useMemo",
155
+ "useCallback",
156
+ "useRef",
157
+ "useContext",
158
+ "useReducer",
159
+ "Fragment",
160
+ ].map((name) => ({
161
+ type: "ImportSpecifier",
162
+ imported: { type: "Identifier", name },
163
+ local: { type: "Identifier", name },
164
+ })),
165
+ source: { type: "Literal", value: "react" },
166
+ },
167
+ ],
168
+ },
169
+ },
170
+ };
171
+
172
+ // ============================================
173
+ // Main Plugin
174
+ // ============================================
175
+
176
+ export function remarkOpendocs() {
177
+ return (tree: Root) => {
178
+ // Inject React hooks import at the beginning
179
+ tree.children.unshift(REACT_HOOKS_IMPORT as any);
180
+
181
+ // Process component-specific transformations
182
+ visit(tree, (node) => {
183
+ if (!isMdxJsxFlowElement(node)) return;
184
+
185
+ switch (node.name) {
186
+ case "CodeGroup":
187
+ processCodeGroup(node);
188
+ break;
189
+ case "Tabs":
190
+ processTabs(node);
191
+ break;
192
+ }
193
+ });
194
+ };
195
+ }