Transpile docs to vue components at build time (#8743)

This commit is contained in:
Nicola Krumschmidt
2021-10-12 20:22:14 +02:00
committed by GitHub
parent 3712892e79
commit 2908063d86
12 changed files with 281 additions and 194 deletions

View File

@@ -1,608 +0,0 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="md" :class="pageClass" @click="onClick" v-html="html" />
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUpdated, inject } from 'vue';
import MarkdownIt from 'markdown-it';
import markdownItTableOfContents from 'markdown-it-table-of-contents';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItContainer from 'markdown-it-container';
import fm from 'front-matter';
import hljs from 'highlight.js';
import hljsGraphQL from '@/utils/hljs-graphql';
import { getRootPath } from '@/utils/get-root-path';
import { useRoute, useRouter } from 'vue-router';
hljs.registerLanguage('graphql', hljsGraphQL);
const md = new MarkdownIt({
html: true,
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (err: any) {
// eslint-disable-next-line no-console
console.warn('There was an error highlighting in Markdown');
// eslint-disable-next-line no-console
console.error(err);
}
}
return '';
},
});
md.use(markdownItTableOfContents, { includeLevel: [2] });
md.use(markdownItAnchor, { permalink: true, permalinkSymbol: '#' });
function hintRenderer(type: string) {
return (tokens: any[], idx: number) => {
const token = tokens[idx];
let title = token.info.trim().slice(type.length).trim() || '';
if (title) title = `<div class="hint-title">${title}</div>`;
if (token.nesting === 1) {
return `<div class="${type} hint">${title}\n`;
} else {
return '</div>\n';
}
};
}
md.use(markdownItContainer, 'tip', { render: hintRenderer('tip') });
md.use(markdownItContainer, 'warning', { render: hintRenderer('warning') });
md.use(markdownItContainer, 'danger', { render: hintRenderer('danger') });
export default defineComponent({
setup(props, { slots }) {
const router = useRouter();
const route = useRoute();
const html = ref('');
const pageClass = ref<string>();
onMounted(generateHTML);
onUpdated(generateHTML);
return { html, onClick, pageClass };
function generateHTML() {
const source = slots.default?.()[0].children;
if (!source || typeof source !== 'string') {
html.value = '';
return;
}
const { attributes, body } = fm<{ pageClass?: string }>(source);
let markdown = body;
const rawImages = body.matchAll(/!\[[^\]]*\]\((?<filename>.*?)(?="|\))(?<optionalpart>".*")?\)/g) ?? [];
const rootPath = getRootPath();
for (const rawImage of rawImages) {
const filenameParts = rawImage.groups!.filename.split('/');
while (filenameParts.includes('assets')) {
filenameParts.shift();
}
const newFilename = `${rootPath}admin/img/docs/${filenameParts.join('/')}`;
const newImage = rawImage[0].replace(rawImage.groups!.filename, newFilename);
markdown = markdown.replace(rawImage[0], newImage);
}
pageClass.value = attributes?.pageClass;
const htmlString = md.render(markdown);
html.value = htmlString;
// The Markdown is fetched async on page transition, which means the # link already exists before the markdown does
// This will force the main el to scroll down to the targetted element on updates of the content
const mainElement = inject('main-element', ref<Element | null>(null));
if (route.hash) {
const linkedEl = document.querySelector(route.hash) as HTMLElement;
if (linkedEl) {
mainElement.value?.scrollTo({ top: linkedEl.offsetTop - 100 });
}
}
}
function onClick(event: MouseEvent) {
if (
event.target &&
(event.target as HTMLElement).tagName.toLowerCase() === 'a' &&
(event.target as HTMLAnchorElement).href
) {
const link = (event.target as HTMLAnchorElement).getAttribute('href')!;
if (link.startsWith('http') || link.startsWith('#')) return;
event.preventDefault();
const parts = link.split('#');
parts[0] = parts[0].endsWith('/') ? parts[0].slice(0, -1) : parts[0];
router.push({
path: `/docs${parts[0]}`,
hash: `#${parts[1]}`,
});
}
}
},
});
</script>
<style scoped>
.error {
padding: 20vh 0;
}
.md {
max-width: 740px;
color: var(--foreground-normal-alt);
font-weight: 400;
font-size: 16px;
line-height: 27px;
}
.md > :deep(*:first-child) {
margin-top: 0;
}
.md > :deep(*:last-child) {
margin-bottom: 0;
}
.md :deep(a) {
color: var(--primary-110);
font-weight: 500;
text-decoration: none;
}
.md :deep(h1),
.md :deep(h2),
.md :deep(h3),
.md :deep(h4),
.md :deep(h5),
.md :deep(h6) {
position: relative;
margin: 40px 0 8px;
padding: 0;
color: var(--foreground-normal-alt);
font-weight: 700;
cursor: text;
}
.md :deep(h1 a),
.md :deep(h2 a),
.md :deep(h3 a),
.md :deep(h4 a),
.md :deep(h5 a),
.md :deep(h6 a) {
position: absolute;
right: 100%;
padding-right: 4px;
opacity: 0;
}
.md :deep(h1) {
margin-bottom: 40px;
font-size: 35px;
line-height: 44px;
}
.md :deep(h2) {
margin-top: 60px;
margin-bottom: 20px;
padding-bottom: 4px;
font-size: 24px;
line-height: 34px;
border-bottom: 2px solid var(--border-subdued);
}
.md :deep(h3) {
margin-bottom: 0px;
font-size: 19px;
line-height: 24px;
}
.md :deep(h4) {
font-size: 16px;
}
.md :deep(h5) {
font-size: 14px;
}
.md :deep(h6) {
color: var(--foreground-normal);
font-size: 14px;
}
.md :deep(pre) {
padding: 16px 20px;
overflow: auto;
font-size: 13px;
line-height: 24px;
background-color: var(--background-normal);
border-radius: var(--border-radius);
}
.md :deep(code),
.md :deep(tt) {
margin: 0 1px;
padding: 0 4px;
font-size: 15px;
font-family: var(--family-monospace);
white-space: nowrap;
background-color: var(--background-page);
border: 1px solid var(--background-normal);
border-radius: var(--border-radius);
}
.md :deep(pre code) {
margin: 0;
padding: 0;
white-space: pre;
background: transparent;
border: none;
}
.md :deep(p) {
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
}
.md :deep(h3 + p) {
margin-block-start: 0.5em;
}
.md > :deep(h2:first-child) {
margin-top: 0;
padding-top: 0;
}
.md > :deep(h1:first-child) {
margin-top: 0;
padding-top: 0;
}
.md > :deep(h3:first-child),
.md > :deep(h4:first-child),
.md > :deep(h5:first-child),
.md > :deep(h6:first-child) {
margin-top: 0;
padding-top: 0;
}
.md :deep(blockquote) {
max-width: 740px;
margin-bottom: 4rem;
padding: 0.25rem 0 0.25rem 1rem;
color: var(--foreground-subdued);
font-size: 18px;
border-left: 2px solid var(--background-normal);
}
.md :deep(blockquote > :first-child) {
margin-top: 0;
}
.md :deep(blockquote > :last-child) {
margin-bottom: 0;
}
.md :deep(table) {
min-width: 100%;
margin: 40px 0;
padding: 0;
border-collapse: collapse;
border-spacing: 0;
}
.md :deep(img) {
max-width: 100%;
margin: 20px 0;
border-radius: 6px;
}
.md :deep(table img) {
margin: 0;
}
.md :deep(table tr) {
margin: 0;
padding: 0;
border-top: 1px solid var(--border-normal);
}
.md :deep(table tr:nth-child(2n)) {
background-color: var(--background-page);
}
.md :deep(table tr th) {
margin: 0;
padding: 8px 20px;
font-weight: bold;
text-align: left;
border: 1px solid var(--border-normal);
}
.md :deep(table tr td) {
margin: 0;
padding: 8px 20px;
text-align: left;
border: 1px solid var(--border-normal);
}
.md :deep(a:first-child h1),
.md :deep(a:first-child h2),
.md :deep(a:first-child h3),
.md :deep(a:first-child h4),
.md :deep(a:first-child h5),
.md :deep(a:first-child h6) {
margin-top: 0;
padding-top: 0;
}
.md :deep(table tr th :first-child),
.md :deep(table tr td :first-child) {
margin-top: 0;
}
.md :deep(table tr th :last-child),
.md :deep(table tr td :last-child) {
margin-bottom: 0;
}
.md :deep(h1 a:hover),
.md :deep(h2 a:hover),
.md :deep(h3 a:hover),
.md :deep(h4 a:hover),
.md :deep(h5 a:hover),
.md :deep(h6 a:hover) {
text-decoration: underline;
}
.md :deep(h1:hover a),
.md :deep(h2:hover a),
.md :deep(h3:hover a),
.md :deep(h4:hover a),
.md :deep(h5:hover a),
.md :deep(h6:hover a) {
opacity: 1;
}
.md :deep(pre code),
.md :deep(pre tt) {
background-color: transparent;
border: none;
}
.md :deep(h1 tt),
.md :deep(h1 code),
.md :deep(h2 tt),
.md :deep(h2 code),
.md :deep(h3 tt),
.md :deep(h3 code),
.md :deep(h4 tt),
.md :deep(h4 code),
.md :deep(h5 tt),
.md :deep(h5 code),
.md :deep(h6 tt),
.md :deep(h6 code) {
font-size: inherit;
}
.md :deep(h1 p),
.md :deep(h2 p),
.md :deep(h3 p),
.md :deep(h4 p),
.md :deep(h5 p),
.md :deep(h6 p) {
margin-top: 0;
}
.md :deep(ul),
.md :deep(ol) {
margin: 20px 0;
padding-left: 20px;
}
.md :deep(ul li),
.md :deep(ol li) {
margin: 8px 0;
line-height: 24px;
}
.md :deep(ul ul),
.md :deep(ul ol),
.md :deep(ol ul),
.md :deep(ol ol) {
margin: 4px 0;
}
.md :deep(ul ul li),
.md :deep(ul ol li),
.md :deep(ol ul li),
.md :deep(ol ol li) {
margin: 4px 0;
line-height: 24px;
}
.md :deep(img.no-margin) {
margin: 0;
}
.md :deep(img.full) {
width: 100%;
}
.md :deep(img.shadow) {
box-shadow: 0px 5px 10px 0px rgba(23, 41, 64, 0.1), 0px 2px 40px 0px rgba(23, 41, 64, 0.05);
}
.md.page-reference {
max-width: 1200px;
}
.md.page-reference :deep(hr) {
position: relative;
left: -2.5rem;
width: calc(100% + 5rem);
margin: 3rem 0;
}
.md.page-reference :deep(h2) {
margin-top: 3rem;
font-size: 2rem;
border-bottom: 0;
}
.md.page-reference :deep(h3) {
margin-top: 3rem;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.md.page-reference :deep(h4) {
margin-top: 2rem;
margin-bottom: 0;
}
.md :deep(.heading-link) {
color: var(--foreground-subdued);
font-size: 16px;
}
.md :deep(.heading-link:hover) {
color: var(--primary-110);
text-decoration: none;
}
.md :deep(li p.first) {
display: inline-block;
}
.md :deep(.table-of-contents ul),
.md :deep(.table-of-contents ol) {
margin-top: 0;
}
.md :deep(.table-of-contents ul li),
.md :deep(.table-of-contents ol li) {
margin: 4px 0;
}
.md :deep(.hint) {
display: inline-block;
width: 100%;
margin: 20px 0;
padding: 0 20px;
background-color: var(--background-subdued);
border-left: 2px solid var(--primary);
}
.md :deep(.two-up) {
margin-top: 3rem;
}
.md :deep(.table-of-contents) {
margin-top: -20px;
}
.md :deep(.hint-title) {
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
font-weight: bold;
}
.md :deep(.hint.tip) {
border-left: 2px solid var(--primary);
}
.md :deep(.hint.warning) {
background-color: var(--warning-10);
border-left: 2px solid var(--warning);
}
.md :deep(.hint.danger) {
background-color: var(--danger-10);
border-left: 2px solid var(--danger);
}
.md :deep(.two-up .right) {
margin-top: 50px;
}
.md :deep(.two-up .right h5) {
margin-top: 20px;
color: var(--foreground-subdued);
}
.md.page-reference :deep(.definitions) {
font-size: 0.9rem;
line-height: 1.5rem;
}
.md.page-reference :deep(.definitions > p) {
margin: 0;
padding: 0.8rem 0;
border-bottom: 2px solid var(--border-subdued);
}
.md.page-reference :deep(.definitions > p:first-child) {
border-top: 2px solid var(--border-subdued);
}
.md.page-reference :deep(.definitions > p > code:first-child) {
margin-right: 0.2rem;
padding: 0;
font-weight: 700;
font-size: 0.9rem;
background: transparent;
border: 0;
}
.md.page-reference :deep(.definitions > p > strong) {
color: var(--foreground-subdued);
}
@media (min-width: 1000px) {
.md :deep(.two-up) {
display: grid;
grid-gap: 40px;
grid-template-columns: minmax(0, 4fr) minmax(0, 3fr);
align-items: start;
}
.md :deep(.two-up .right) {
position: sticky;
top: 100px;
margin-top: 0;
}
.md :deep(.two-up .left > *:first-child),
.md :deep(.two-up .right > *:first-child) {
margin-top: 0 !important;
}
}
</style>

View File

@@ -11,9 +11,15 @@ export default defineModule({
routes: [
{
path: '',
redirect: '/docs/getting-started/introduction/',
component: StaticDocs,
children: [
{
path: '',
redirect: '/docs/getting-started/introduction/',
},
...getRoutes(docs),
],
},
...getRoutes(docs),
{
path: ':_(.+)+',
component: NotFound,
@@ -29,15 +35,12 @@ function getRoutes(routes: DocsRoutes): RouteRecordRaw[] {
updatedRoutes.push({
name: `docs-${route.path.replace('/', '-')}`,
path: route.path,
component: StaticDocs,
meta: {
import: route.import,
},
component: route.import,
});
} else {
updatedRoutes.push({
path: route.path,
redirect: `/docs${route.children[0].path}`,
redirect: `/docs/${route.children[0].path}`,
});
updatedRoutes.push(...getRoutes(route.children));

View File

@@ -16,7 +16,7 @@
</template>
<div class="docs-content selectable">
<markdown>{{ markdownWithoutTitle }}</markdown>
<router-view @update:title="title = $event" @update:modular-extension="modularExtension = $event" />
</div>
<template #sidebar>
@@ -29,65 +29,22 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, defineAsyncComponent, ref, computed, watch } from 'vue';
import { useRoute, RouteLocation } from 'vue-router';
import { defineComponent, ref } from 'vue';
import { useRoute } from 'vue-router';
import DocsNavigation from '../components/navigation.vue';
const Markdown = defineAsyncComponent(() => import('../components/markdown.vue'));
async function getMarkdownForRoute(route: RouteLocation): Promise<string> {
const importDocs = route.meta.import as () => Promise<{ default: string }>;
const mdModule = await importDocs();
return mdModule.default;
}
export default defineComponent({
name: 'StaticDocs',
components: { DocsNavigation, Markdown },
components: { DocsNavigation },
setup() {
const { t } = useI18n();
const route = useRoute();
const markdown = ref('');
const title = computed(() => {
const lines = markdown.value.split('\n');
const line = lines.find((line) => line.startsWith('# '));
const title = ref('Test');
const modularExtension = ref(false);
if (line === undefined) return null;
return line.substring(2).replaceAll('<small></small>', '').trim();
});
const modularExtension = computed(() => {
const lines = markdown.value.split('\n');
const line = lines.find((line) => line.startsWith('# '));
return line?.includes('<small></small>') || false;
});
const markdownWithoutTitle = computed(() => {
const lines = markdown.value.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('# ')) {
lines.splice(i, 1);
break;
}
}
return lines.join('\n');
});
watch(
() => route.path,
async () => {
markdown.value = await getMarkdownForRoute(route);
},
{ immediate: true, flush: 'post' }
);
return { t, route, markdown, title, markdownWithoutTitle, modularExtension };
return { t, route, title, modularExtension };
},
});
</script>