UNPKG

@ztl-uwu/nuxt-content

Version:

Write your content inside your Nuxt app

1,177 lines (1,169 loc) 31.1 kB
import * as _nuxt_schema from '@nuxt/schema'; import { StorageValue } from 'unstorage'; import { LayoutKey } from '#build/types/layouts'; import { Options } from 'minisearch'; import { ListenOptions } from 'listhen'; import { BuiltinTheme, BuiltinLanguage, LanguageRegistration, ThemeRegistrationAny } from 'shiki'; interface ParsedContentInternalMeta { /** * Content id */ _id: string; /** * Content source */ _source?: string; /** * Content path, this path is source agnostic and it the content my live in any source */ _path?: string; /** * Content title */ title?: string; /** * Content draft status */ _draft?: boolean; /** * Content partial status */ _partial?: boolean; /** * Content locale */ _locale?: string; /** * File type of the content, i.e `markdown` */ _type?: 'markdown' | 'yaml' | 'json' | 'csv'; /** * Path to the file relative to the content directory */ _file?: string; /** * Extension of the file */ _extension?: 'md' | 'yaml' | 'yml' | 'json' | 'json5' | 'csv'; } interface TocLink { id: string; text: string; depth: number; children?: TocLink[]; } interface Toc { title: string; depth: number; searchDepth: number; links: TocLink[]; } interface MarkdownNode { type: string; tag?: string; value?: string; props?: Record<string, any>; content?: any; children?: MarkdownNode[]; attributes?: Record<string, any>; fmAttributes?: Record<string, any>; } interface MarkdownRoot { type: 'root'; children: MarkdownNode[]; props?: Record<string, any>; toc?: Toc; } interface MarkdownHtmlNode extends MarkdownNode { type: 'html'; value: string; } type MarkdownPlugin = Record<string, any>; interface MarkdownOptions { /** * Enable/Disable MDC components. */ mdc: boolean; toc: { /** * Maximum heading depth to include in the table of contents. */ depth: number; searchDepth: number; }; tags: Record<string, string>; remarkPlugins: Record<string, false | (MarkdownPlugin & { instance: any; })>; rehypePlugins: Record<string, false | (MarkdownPlugin & { instance: any; })>; } interface ParsedContentMeta extends ParsedContentInternalMeta { /** * Layout */ layout?: LayoutKey; [key: string]: any; } interface ParsedContent extends ParsedContentMeta { /** * Excerpt */ excerpt?: MarkdownRoot; /** * Content body */ body: MarkdownRoot | null; } interface MarkdownParsedContent extends ParsedContent { _type: 'markdown'; /** * Content is empty */ _empty: boolean; /** * Content description */ description: string; /** * Content excerpt, generated from content */ excerpt?: MarkdownRoot; /** * Parsed Markdown body with included table of contents. */ body: MarkdownRoot & { toc?: Toc; }; } interface ContentTransformer { name: string; extensions: string[]; parse?(id: string, content: StorageValue, options: any): Promise<ParsedContent> | ParsedContent; transform?(content: ParsedContent, options: any): Promise<ParsedContent> | ParsedContent; } interface TransformContentOptions { transformers?: ContentTransformer[]; [key: string]: any; } /** * Query */ interface SortParams { /** * Locale specifier for sorting * A string with a BCP 47 language tag * * @default undefined */ $locale?: string; /** * Whether numeric collation should be used, such that "1" < "2" < "10". * Possible values are `true` and `false`; * * @default false */ $numeric?: boolean; /** * Whether upper case or lower case should sort first. * Possible values are `"upper"`, `"lower"`, or `"false"` * * @default "depends on locale" */ $caseFirst?: 'upper' | 'lower' | 'false'; /** * Which differences in the strings should lead to non-zero result values. Possible values are: * - "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A. * - "accent": Only strings that differ in base letters or accents and other diacritic marks compare as unequal. Examples: a ≠ b, a ≠ á, a = A. * - "case": Only strings that differ in base letters or case compare as unequal. Examples: a ≠ b, a = á, a ≠ A. * - "variant": Strings that differ in base letters, accents and other diacritic marks, or case compare as unequal. Other differences may also be taken into consideration. Examples: a ≠ b, a ≠ á, a ≠ A. * * @default "variant" */ $sensitivity?: 'base' | 'accent' | 'case' | 'variant'; } interface SortFields { [field: string]: -1 | 1; } type SortOptions = SortParams | SortFields; interface QueryBuilderWhere extends Partial<Record<keyof ParsedContentInternalMeta, string | number | boolean | RegExp | QueryBuilderWhere>> { /** * Match only if all of nested conditions are true * * @example ```ts queryContent().where({ $and: [ { score: { $gte: 5 } }, { score: { $lte: 10 } } ] }) ``` **/ $and?: QueryBuilderWhere[]; /** * Match if any of nested conditions is true * * @example ```ts queryContent().where({ $or: [ { score: { $gt: 5 } }, { score: { $lt: 3 } } ] }) ``` **/ $or?: QueryBuilderWhere[]; /** * Match is condition is false * * @example ```ts queryContent().where({ title: { $not: 'Hello World' } }) ``` **/ $not?: string | number | boolean | RegExp | QueryBuilderWhere; /** * Match if item equals condition * * @example ```ts queryContent().where({ title: { $eq: 'Hello World' } }) ``` **/ $eq?: string | number | boolean | RegExp; /** * Match if item not equals condition * * @example ```ts queryContent().where({ score: { $ne: 100 } }) ``` **/ $ne?: string | number | boolean | RegExp; /** * Check if item is greater than condition * * @example ```ts queryContent().where({ score: { $gt: 99.5 } }) ``` */ $gt?: number; /** * Check if item is greater than or equal to condition * * @example ```ts queryContent().where({ score: { $gte: 99.5 } }) ``` */ $gte?: number; /** * Check if item is less than condition * * @example ```ts queryContent().where({ score: { $lt: 99.5 } }) ``` */ $lt?: number; /** * Check if item is less than or equal to condition * * @example ```ts queryContent().where({ score: { $lte: 99.5 } }) ``` */ $lte?: number; /** * Provides regular expression capabilities for pattern matching strings. * * @example ```ts queryContent().where({ title: { $regex: /^foo/ } }) ``` */ $regex?: RegExp | string; /** * Match if type of item equals condition * * @example ```ts queryContent().where({ field: { $type: 'boolean' } }) ``` */ $type?: string; /** * Check key existence * * @example ```ts queryContent().where({ tag: { $exists: false } }) ``` */ $exists?: boolean; /** * Match if item contains every condition or match every rule in condition array * * @example ```ts queryContent().where({ title: { $contains: ['Hello', 'World'] } }) ``` **/ $contains?: Array<string | number | boolean> | string | number | boolean; /** * Match if item contains at least one rule from condition array * * @example ```ts queryContent().where({ title: { $containsAny: ['Hello', 'World'] } }) ``` */ $containsAny?: Array<string | number | boolean>; /** * Ignore case contains * * @example ```ts queryContent().where({ title: { $icontains: 'hello world' } }) ``` **/ $icontains?: string; /** * Match if item is in condition array * * @example ```ts queryContent().where({ category: { $in: ['sport', 'nature', 'travel'] } }) ``` **/ $in?: string | Array<string | number | boolean>; [key: string]: undefined | string | number | boolean | RegExp | QueryBuilderWhere | Array<string | number | boolean | QueryBuilderWhere>; } interface QueryBuilderParams { first?: boolean; skip?: number; limit?: number; only?: string[]; without?: string[]; sort?: SortOptions[]; where?: QueryBuilderWhere[]; surround?: { query: string | QueryBuilderWhere; before?: number; after?: number; }; [key: string]: any; } interface QueryBuilder<T = ParsedContentMeta> { /** * Select a subset of fields */ only<const K extends keyof T>(keys: K): QueryBuilder<Pick<T, K>>; only<const K extends (keyof T)[]>(keys: K): QueryBuilder<Pick<T, K[number]>>; /** * Remove a subset of fields */ without<const K extends keyof T | string>(keys: K): QueryBuilder<Omit<T, K>>; without<const K extends (keyof T | string)[]>(keys: K): QueryBuilder<Omit<T, K[number]>>; /** * Sort results */ sort(options: SortOptions): QueryBuilder<T>; /** * Filter results */ where(query: QueryBuilderWhere): QueryBuilder<T>; /** * Limit number of results */ limit(count: number): QueryBuilder<T>; /** * Skip number of results */ skip(count: number): QueryBuilder<T>; /** * Fetch list of contents */ find(): Promise<Array<T>>; /** * Fetch first matched content */ findOne(): Promise<T>; /** * Fetch sorround contents */ findSurround(query: string | QueryBuilderWhere, options?: Partial<{ before: number; after: number; }>): Promise<Array<T>>; /** * Count matched contents */ count(): Promise<number>; /** * Filter contents based on locale */ locale(locale: string): QueryBuilder<T>; /** * Retrieve query builder params * @internal */ params: () => QueryBuilderParams; } type QueryPipe<T = any> = ((data: Array<T>, param: QueryBuilderParams) => Array<T>) | ((data: Array<T>, param: QueryBuilderParams) => void); type DatabaseFetcher<T> = (query: QueryBuilder<T>) => Promise<Array<T> | T>; type QueryMatchOperator = (item: any, condition: any) => boolean; interface NavItem { title: string; _path: string; _id?: string; _draft?: boolean; children?: NavItem[]; [key: string]: any; } type MountOptions = { driver: 'fs' | 'http' | string; name?: string; prefix?: string; [options: string]: any; }; interface ModuleOptions { /** * Base route that will be used for content api * * @default '_content' * @deprecated Use `api.base` instead */ base: string; api: { /** * Base route that will be used for content api * * @default '/api/_content' */ baseURL: string; }; /** * Disable content watcher and hot content reload. * Note: Watcher is a development feature and will not includes in the production. * * @default true */ watch: false | { ws: Partial<ListenOptions>; }; /** * Contents can be located in multiple places, in multiple directories or even in remote git repositories. * Using sources option you can tell Content module where to look for contents. * * @default ['content'] */ sources: Record<string, MountOptions> | Array<string | MountOptions>; /** * List of ignore patterns that will be used to exclude content from parsing, rendering and watching. * * Note that files with a leading . or - are ignored by default * * @default [] */ ignores: Array<string>; /** * Content module uses `remark` and `rehype` under the hood to compile markdown files. * You can modify this options to control its behavior. */ markdown: { /** * Whether MDC syntax should be supported or not. * * @default true */ mdc?: boolean; /** * Control behavior of Table of Contents generation */ toc?: { /** * Maximum heading depth that includes in the table of contents. * * @default 2 */ depth?: number; /** * Maximum depth of nested tags to search for heading. * * @default 2 */ searchDepth?: number; }; /** * Tags will be used to replace markdown components and render custom components instead of default ones. * * @default {} */ tags?: Record<string, string>; /** * Register custom remark plugin to provide new feature into your markdown contents. * Checkout: https://github.com/remarkjs/remark/blob/main/doc/plugins.md * * @default [] */ remarkPlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>; /** * Register custom remark plugin to provide new feature into your markdown contents. * Checkout: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md * * @default [] */ rehypePlugins?: Array<string | [string, MarkdownPlugin]> | Record<string, false | MarkdownPlugin>; /** * Anchor link generation config * * @default {} */ anchorLinks?: boolean | { /** * Sets the maximal depth for anchor link generation * * @default 4 */ depth?: number; /** * Excludes headings from link generation when they are in the depth range. * * @default [1] */ exclude?: number[]; }; }; /** * Content module uses `shiki` to highlight code blocks. * You can configure Shiki options to control its behavior. */ highlight: false | { /** * Default theme that will be used for highlighting code blocks. */ theme?: BuiltinTheme | { default: BuiltinTheme; [theme: string]: BuiltinTheme; }; /** * Preloaded languages that will be available for highlighting code blocks. * * @deprecated Use `langs` instead */ preload?: (BuiltinLanguage | LanguageRegistration)[]; /** * Languages to be bundled loaded by Shiki * * All languages used has to be included in this list at build time, to create granular bundles. * * Unlike the `preload` option, when this option is provided, it will override the default languages. * * @default ['js','jsx','json','ts','tsx','vue','css','html','vue','bash','md','mdc','yaml'] */ langs?: (BuiltinLanguage | LanguageRegistration)[]; /** * Additional themes to be bundled loaded by Shiki */ themes?: (BuiltinTheme | ThemeRegistrationAny)[]; }; /** * Options for yaml parser. * * @default {} */ yaml: false | Record<string, any>; /** * Options for yaml parser. * * @default {} */ csv: false | { json?: boolean; delimeter?: string; }; /** * Enable/Disable navigation. * * @default {} */ navigation: false | { fields: Array<string>; }; /** * List of locale codes. * This codes will be used to detect contents locale. * * @default [] */ locales: Array<string>; /** * Default locale for top level contents. * * @default undefined */ defaultLocale?: string; /** * Enable automatic usage of `useContentHead` * * @default true */ contentHead?: boolean; /** * Document-driven mode config * * @default false */ documentDriven: boolean | { host?: string; page?: boolean; navigation?: boolean; surround?: boolean; globals?: { [key: string]: QueryBuilderParams; }; layoutFallbacks?: string[]; injectPage?: boolean; trailingSlash?: boolean; }; /** * Enable to keep uppercase characters in the generated routes. * * @default false */ respectPathCase: boolean; experimental: { clientDB?: boolean; stripQueryParameters?: boolean; advanceQuery?: boolean; /** * Control content cach generation. * * This option might be removed in the next major version. */ cacheContents?: boolean; /** * Search mode. * * @default undefined */ search?: { /** * List of tags where text must not be extracted. * * By default, will extract text from each tag. * * @default ['script', 'style', 'pre'] */ ignoredTags?: Array<string>; /** * Query used to filter contents that must be searched. * @default { _partial: false, _draft: false} */ filterQuery?: QueryBuilderWhere; /** * API return indexed contents to improve client-side load time. * This option will use MiniSearch to create the index. * If you disable this option, API will return raw contents instead * you can use with any client-side search. * * @default true */ indexed?: boolean; /** * MiniSearch Options. When using `indexed` option, * this options will be used to configure MiniSearch * in order to have the same options on both server and client side. * * @default * { * fields: ['title', 'content', 'titles'], * storeFields: ['title', 'content', 'titles'], * searchOptions: { * prefix: true, * fuzzy: 0.2, * boost: { * title: 4, * content: 2, * titles: 1 * } * } * } * * @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#options */ options?: Options; }; }; } interface ContentContext extends ModuleOptions { base: Readonly<string>; transformers: Array<string>; } interface ContentQueryFindResponse<T> { result: Array<T>; skip: number; limit: number; total: number; } interface ContentQueryFindOneResponse<T> { result: T | undefined; } interface ContentQueryCountResponse { result: number; } type ContentQueryResponse<T> = ContentQueryFindResponse<T> | ContentQueryFindOneResponse<T> | ContentQueryCountResponse; interface ContentQueryWithSurround<T> { surround: Array<T | null>; } interface ContentQueryWithDirConfig { dirConfig: Record<string, any>; } /** * Query */ interface ContentQuerySortParams { /** * Locale specifier for sorting * A string with a BCP 47 language tag * * @default undefined */ $locale?: string; /** * Whether numeric collation should be used, such that "1" < "2" < "10". * Possible values are `true` and `false`; * * @default false */ $numeric?: boolean; /** * Whether upper case or lower case should sort first. * Possible values are `"upper"`, `"lower"`, or `"false"` * * @default "depends on locale" */ $caseFirst?: 'upper' | 'lower' | 'false'; /** * Which differences in the strings should lead to non-zero result values. Possible values are: * - "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A. * - "accent": Only strings that differ in base letters or accents and other diacritic marks compare as unequal. Examples: a ≠ b, a ≠ á, a = A. * - "case": Only strings that differ in base letters or case compare as unequal. Examples: a ≠ b, a = á, a ≠ A. * - "variant": Strings that differ in base letters, accents and other diacritic marks, or case compare as unequal. Other differences may also be taken into consideration. Examples: a ≠ b, a ≠ á, a ≠ A. * * @default "variant" */ $sensitivity?: 'base' | 'accent' | 'case' | 'variant'; } interface ContentQuerySortFields { [field: string]: -1 | 1; } type ContentQuerySortOptions = ContentQuerySortParams | ContentQuerySortFields; interface ContentQueryBuilderWhere extends Partial<Record<keyof ParsedContentInternalMeta, string | number | boolean | RegExp | ContentQueryBuilderWhere>> { /** * Match only if all of nested conditions are true * * @example ```ts queryContent().where({ $and: [ { score: { $gte: 5 } }, { score: { $lte: 10 } } ] }) ``` **/ $and?: ContentQueryBuilderWhere[]; /** * Match if any of nested conditions is true * * @example ```ts queryContent().where({ $or: [ { score: { $gt: 5 } }, { score: { $lt: 3 } } ] }) ``` **/ $or?: ContentQueryBuilderWhere[]; /** * Match is condition is false * * @example ```ts queryContent().where({ title: { $not: 'Hello World' } }) ``` **/ $not?: string | number | boolean | RegExp | ContentQueryBuilderWhere; /** * Match if item equals condition * * @example ```ts queryContent().where({ title: { $eq: 'Hello World' } }) ``` **/ $eq?: string | number | boolean | RegExp; /** * Match if item not equals condition * * @example ```ts queryContent().where({ score: { $ne: 100 } }) ``` **/ $ne?: string | number | boolean | RegExp; /** * Check if item is greater than condition * * @example ```ts queryContent().where({ score: { $gt: 99.5 } }) ``` */ $gt?: number | string; /** * Check if item is greater than or equal to condition * * @example ```ts queryContent().where({ score: { $gte: 99.5 } }) ``` */ $gte?: number | string; /** * Check if item is less than condition * * @example ```ts queryContent().where({ score: { $lt: 99.5 } }) ``` */ $lt?: number | string; /** * Check if item is less than or equal to condition * * @example ```ts queryContent().where({ score: { $lte: 99.5 } }) ``` */ $lte?: number | string; /** * Provides regular expression capabilities for pattern matching strings. * * @example ```ts queryContent().where({ title: { $regex: /^foo/ } }) ``` */ $regex?: RegExp | string; /** * Match if type of item equals condition * * @example ```ts queryContent().where({ field: { $type: 'boolean' } }) ``` */ $type?: string; /** * Check key existence * * @example ```ts queryContent().where({ tag: { $exists: false } }) ``` */ $exists?: boolean; /** * Match if item contains every condition or match every rule in condition array * * @example ```ts queryContent().where({ title: { $contains: ['Hello', 'World'] } }) ``` **/ $contains?: Array<string | number | boolean> | string | number | boolean; /** * Match if item contains at least one rule from condition array * * @example ```ts queryContent().where({ title: { $containsAny: ['Hello', 'World'] } }) ``` */ $containsAny?: Array<string | number | boolean>; /** * Ignore case contains * * @example ```ts queryContent().where({ title: { $icontains: 'hello world' } }) ``` **/ $icontains?: string; /** * Match if item is in condition array * * @example ```ts queryContent().where({ category: { $in: ['sport', 'nature', 'travel'] } }) ``` **/ $in?: Array<string | number | boolean>; [key: string]: string | number | boolean | RegExp | ContentQueryBuilderWhere | Array<string | number | boolean | ContentQueryBuilderWhere> | undefined; } interface ContentQueryBuilderParams { first?: boolean; skip?: number; limit?: number; only?: string[]; without?: string[]; sort?: ContentQuerySortOptions[]; where?: ContentQueryBuilderWhere[] | ContentQueryBuilderWhere; surround?: { query: string | ContentQueryBuilderWhere; before?: number; after?: number; }; [key: string]: any; } type ValidKey<T> = keyof T | string; type ValidKeys<T> = Array<keyof T | string>; interface ContentQueryBuilder<T = ParsedContentMeta, Y = Record<string, any>> { /** * Select a subset of fields */ only<T, K extends ValidKey<T>>(keys: K): ContentQueryBuilder<Pick<T, Extract<K, keyof T>>, Y>; only<T, K extends ValidKeys<T>>(keys: K): ContentQueryBuilder<Pick<T, Extract<K[number], keyof T>>, Y>; /** * Remove a subset of fields */ without<const K extends keyof T | string>(keys: K): ContentQueryBuilder<Omit<T, K>, Y>; without<const K extends (keyof T | string)[]>(keys: K): ContentQueryBuilder<Omit<T, K[number]>, Y>; /** * Filter results */ where(query: ContentQueryBuilderWhere): ContentQueryBuilder<T, Y>; /** * Sort results */ sort(options: ContentQuerySortOptions): ContentQueryBuilder<T, Y>; /** * Limit number of results */ limit(count: number): ContentQueryBuilder<T, Y>; /** * Skip number of results */ skip(count: number): ContentQueryBuilder<T, Y>; /** * Retrieve query builder params * @internal */ params: () => ContentQueryBuilderParams; /** * Filter contents based on locale */ locale(locale: string): ContentQueryBuilder<T, Y>; /** * Fetch list of contents */ find(): Promise<ContentQueryFindResponse<T> & Y>; /** * Fetch first matched content */ findOne(): Promise<ContentQueryFindOneResponse<T> & Y>; /** * Count matched contents */ count(): Promise<ContentQueryCountResponse>; /** * Fetch sorround contents */ withSurround(query: string | ContentQueryBuilderWhere, options?: Partial<{ before: number; after: number; }>): ContentQueryBuilder<T, ContentQueryWithSurround<T>>; /** * Fetch sorround contents */ withDirConfig(): ContentQueryBuilder<T, ContentQueryWithDirConfig>; } type ContentQueryFetcher<T> = (query: ContentQueryBuilder<T>) => Promise<ContentQueryResponse<T>>; declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>; interface ModuleHooks { 'content:context'(ctx: ContentContext): void; } interface ModulePublicRuntimeConfig { experimental: { stripQueryParameters: boolean; clientDB: boolean; advanceQuery: boolean; }; api: { baseURL: string; }; host: string | undefined; trailingSlash: boolean; integrity: number | undefined; respectPathCase: boolean; defaultLocale: ModuleOptions['defaultLocale']; locales: ModuleOptions['locales']; tags: Record<string, string>; base: string; wsUrl?: string; highlight: ModuleOptions['highlight']; navigation: ModuleOptions['navigation']; search: ModuleOptions['experimental']['search']; contentHead: ModuleOptions['contentHead']; documentDriven: ModuleOptions['documentDriven']; } interface ModulePrivateRuntimeConfig { /** * Internal version that represents cache format. * This is used to invalidate cache when the format changes. */ cacheVersion: string; cacheIntegrity: string; } declare module '@nuxt/schema' { interface PublicRuntimeConfig { content: ModulePublicRuntimeConfig; } interface PrivateRuntimeConfig { content: ModulePrivateRuntimeConfig & ContentContext; } } declare module 'nitropack' { interface NitroRuntimeHooks { 'content:file:beforeParse': (file: { _id: string; body: string; }) => void; 'content:file:afterParse': (file: ParsedContent) => void; } } export { _default as default }; export type { ContentContext, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryCountResponse, ContentQueryFetcher, ContentQueryFindOneResponse, ContentQueryFindResponse, ContentQueryResponse, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentQueryWithDirConfig, ContentQueryWithSurround, ContentTransformer, DatabaseFetcher, MarkdownHtmlNode, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPlugin, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, QueryBuilder, QueryBuilderParams, QueryBuilderWhere, QueryMatchOperator, QueryPipe, SortFields, SortOptions, SortParams, Toc, TocLink, TransformContentOptions };