1import {PluginOptions, Guide} from './types';
2import {LoadContext} from '@docusaurus/types';
3
4import _ from 'lodash';
5import fs from 'fs-extra';
6import globby from 'globby';
7import humanizeString from 'humanize-string';
8import path from 'path';
9import {parse, normalizeUrl, aliasedSitePath} from '@docusaurus/utils';
10import readingTime from 'reading-time';
11import titleize from 'titleize';
12
13export function truncate(fileString: string, truncateMarker: RegExp) {
14  return fileString.split(truncateMarker, 1).shift()!;
15}
16
17export async function generateGuides(
18  guideDir: string,
19  {siteConfig, siteDir}: LoadContext,
20  options: PluginOptions,
21) {
22  const {include, routeBasePath, truncateMarker} = options;
23
24  if (!fs.existsSync(guideDir)) {
25    return [];
26  }
27
28  const {baseUrl = ''} = siteConfig;
29  const guideFiles = await globby(include, {
30    cwd: guideDir,
31  });
32
33  const guides: Guide[] = [];
34
35  await Promise.all(
36    guideFiles.map(async (relativeSource: string) => {
37      const source = path.join(guideDir, relativeSource);
38      const aliasedSource = aliasedSitePath(source, siteDir);
39      const fileString = await fs.readFile(source, 'utf-8');
40      const readingStats = readingTime(fileString);
41      const {frontMatter, content, excerpt} = parse(fileString);
42
43      if (frontMatter.draft && process.env.NODE_ENV === 'production') {
44        return;
45      }
46
47      let categoryParts = relativeSource.split('/').slice(0, -1);
48      let categories = [];
49
50      while (categoryParts.length > 0) {
51        let name = categoryParts[categoryParts.length - 1];
52        let title = titleize(humanizeString(name));
53
54        let description = null;
55
56        switch(name) {
57          case 'advanced':
58            description = 'Go beyond the basics, become a Vector pro, and extract the full potential of Vector.';
59            break;
60
61          case 'getting-started':
62            description = 'Take Vector from zero to production in under 10 minutes.';
63            break;
64
65          case 'integrate':
66            description = 'Simple step-by-step integration guides.'
67            break;
68        }
69
70        categories.unshift({
71          name: name,
72          title: title,
73          description: description,
74          permalink: normalizeUrl([baseUrl, routeBasePath, categoryParts.join('/')])
75        });
76        categoryParts.pop();
77      }
78
79      let linkName = relativeSource.replace(/\.mdx?$/, '');
80      let seriesPosition = frontMatter.series_position;
81      let tags = frontMatter.tags || [];
82      let title = frontMatter.title || linkName;
83      let coverLabel = frontMatter.cover_label || title;
84
85      guides.push({
86        id: frontMatter.id || frontMatter.title,
87        metadata: {
88          categories: categories,
89          coverLabel: coverLabel,
90          description: frontMatter.description || excerpt,
91          permalink: normalizeUrl([
92            baseUrl,
93            routeBasePath,
94            frontMatter.id || linkName,
95          ]),
96          readingTime: readingStats.text,
97          seriesPosition: seriesPosition,
98          sort: frontMatter.sort,
99          source: aliasedSource,
100          tags: tags,
101          title: title,
102          truncated: truncateMarker?.test(content) || false,
103        },
104      });
105    }),
106  );
107
108  return _.sortBy(guides, [
109    ((guide) => {
110      let categories = guide.metadata.categories;
111
112      if (categories[0].name == 'getting-started') {
113        return ['AA'].concat(categories.map(category => category.name).slice(1));
114      } else {
115        return categories;
116      }
117    }),
118    'metadata.seriesPosition',
119    ((guide) => guide.metadata.coverLabel.toLowerCase())
120  ]);
121}
122
123export function linkify(
124  fileContent: string,
125  siteDir: string,
126  guidePath: string,
127  guides: Guide[],
128) {
129  let fencedBlock = false;
130  const lines = fileContent.split('\n').map(line => {
131    if (line.trim().startsWith('```')) {
132      fencedBlock = !fencedBlock;
133    }
134
135    if (fencedBlock) return line;
136
137    let modifiedLine = line;
138    const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
139    let mdMatch = mdRegex.exec(modifiedLine);
140
141    while (mdMatch !== null) {
142      const mdLink = mdMatch[1];
143      const aliasedPostSource = `@site/${path.relative(
144        siteDir,
145        path.resolve(guidePath, mdLink),
146      )}`;
147      let guidePermalink = null;
148
149      guides.forEach(guide => {
150        if (guide.metadata.source === aliasedPostSource) {
151          guidePermalink = guide.metadata.permalink;
152        }
153      });
154
155      if (guidePermalink) {
156        modifiedLine = modifiedLine.replace(mdLink, guidePermalink);
157      }
158
159      mdMatch = mdRegex.exec(modifiedLine);
160    }
161
162    return modifiedLine;
163  });
164
165  return lines.join('\n');
166}
167