Skip to content

Commit

Permalink
Add support for fenced files markdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed Feb 25, 2024
1 parent a7d3bff commit a918ce1
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 107 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"markdown-it-container": "^4.0.0",
"markdown-it-prism": "^2.3.0",
"postcss": "^8.4.35",
"prismjs": "^1.29.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.17.5",
Expand Down
4 changes: 4 additions & 0 deletions src/_includes/component-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- [Vue Components in Markdown](/posts/markdown-components-in-vue)
- [React Components in Markdown](https://press-react.servicestack.net/posts/markdown-components-in-react)
- [.NET Razor SSG Vue Components in Markdown](https://razor-ssg.web-templates.io/posts/javascript)
- [.NET Blazor Vue Components in Markdown](https://blazor-vue.web-templates.io/posts/javascript)
211 changes: 108 additions & 103 deletions src/_posts/2024-03-01_vite-press-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ and rendered with [Markdig](https://github.com/xoofx/markdig) and either Razor P

The content for each Markdown feature is maintained within its own feature folder with a `_` prefix:

<FileLayout :files="{
_includes: {},
_posts: {},
_videos: {},
_whatsnew: {},
}"/>
```files
/_includes
/_posts
/_videos
/_whatsnew
```

#### Markdown Document Structure

Expand Down Expand Up @@ -146,14 +146,12 @@ This `VirtualPress` metadata is used to power all markdown features.
The blog maintains its markdown posts in a flat [/_posts](https://github.com/NetCoreTemplates/vue-spa/tree/main/MyApp.Client/src/_posts)
folder which each Markdown post containing its publish date and URL slug it should be published under, e.g:

<FileLayout :files="{
_posts: { _: [
'...',
'2023-01-21_start.md',
'2024-02-11_jwt-identity-auth.md',
'2024-03-01_vite-press-plugin.md',
]},
}"/>
```files
/_posts
2023-01-21_start.md
2024-02-11_jwt-identity-auth.md
2024-03-01_vite-press-plugin.md
```

Supporting all Blog features requires several different pages to render each of its view:

Expand Down Expand Up @@ -217,13 +215,15 @@ The [/whatsnew](/whatsnew) page is an example of creating a custom Markdown feat
where a new folder is created per release, containing both release date and release or project name, with all features in that release
maintained markdown content sorted in alphabetical order:

<FileLayout :files="{
_whatsnew: {
'2023-03-08_Animaginary': { _: ['feature1.md'] },
'2023-03-18_OpenShuttle': { _: ['feature1.md'] },
'2023-03-28_Planetaria': { _: ['feature1.md'] },
}
}"/>
```files
/_whatsnew
/2023-03-08_Animaginary
feature1.md
/2023-03-18_OpenShuttle
feature1.md
/2023-03-28_Planetaria
feature1.md
```

What's New follows the same structure as Pages feature which is rendered in:

Expand All @@ -234,16 +234,17 @@ What's New follows the same structure as Pages feature which is rendered in:

The videos feature maintained in the `_videos` folder allows grouping of related videos into different folder groups, e.g:

<FileLayout :files="{
_videos: {
'vue': {
_: ['admin.md','autoquerygrid.md','components.md']
},
'react': {
_: ['locode.md','bookings.md','nextjs.md']
},
}
}"/>
```files
/_videos
/vue
admin.md
autoquerygrid.md
components.md
/react
locode.md
bookings.md
nextjs.md
```

These can then be rendered as UI fragments using the `<VideoGroup>` component, e.g:

Expand All @@ -259,15 +260,13 @@ These can then be rendered as UI fragments using the `<VideoGroup>` component, e

The includes feature allows maintaining reusable markdown fragments in the `_includes` folder, e.g:

<FileLayout :files="{
_includes: {
'features': {
_: ['videos.md','whatsnew.md']
},
_: ['privacy.md']
}
}"/>

```files
/_includes
/features
videos.md
whatsnew.md
privacy.md
```

Which can be included in other Markdown files with:

Expand Down Expand Up @@ -303,14 +302,23 @@ export default defineConfig({
This will publish all the content of each content type in the year they were published in, along with an `all.json` containing
all content published in that year as well aso for all time, e.g:

<FileLayout :files="{
meta: {
2022: { _: ['all.json','posts.json','videos.json'] },
2023: { _: ['all.json','posts.json'] },
2024: { _: ['all.json','posts.json','videos.json','whatsnew.json'] },
_: ['all.json','index.json']
}
}"/>
```files
/meta
/2022
all.json
posts.json
videos.json
/2023
all.json
posts.json
/2024
all.json
posts.json
videos.json
whatsnew.json
all.json
index.json
```

With this you can fetch the metadata of all the new **Blog Posts** added in **2023** from:

Expand All @@ -328,53 +336,6 @@ This feature makes it possible to support use-cases like CreatorKit's
[Generating Newsletters](https://servicestack.net/creatorkit/portal-mailruns#generating-newsletters) feature which generates
a Monthly Newsletter Email with all new content added within a specified period.

## Components in Markdown Pages

The [Simple, Modern JavaScript](/posts/javascript) blog post is a good example showing how you can import and reference components in Markdown pages:

```tsx
<script setup>
import Hello from "./components/Hello.vue";
import Counter from "./components/Counter.vue";
import Plugin from "./components/Plugin.vue";
import HelloApi from "./components/HelloApi.vue";
import VueComponentGallery from "./components/VueComponentGallery.vue";
import VueComponentLibrary from "./components/VueComponentLibrary.vue";
</script>

<hello name="Vue 3"></hello>
<counter></counter>
```

As well as use Global Components which don't need to be imported, e.g:

```xml
<FileLayout :files="{
_videos: {
'vue': {
_: ['admin.md','autoquerygrid.md']
},
'react': {
_: ['locode.md','bookings.md']
},
}
}" />
```

#### Output

<FileLayout :files="{
_videos: {
'vue': {
_: ['admin.md','autoquerygrid.md']
},
'react': {
_: ['locode.md','bookings.md']
},
}
}" />


## Markdown Containers

Most of [VitePress Containers](https://vitepress.dev/guide/markdown#custom-containers) are also implemented, enabling
Expand Down Expand Up @@ -422,11 +383,9 @@ You can specify a custom title by appending the text right after the container t

#### Input

```markdown
:::danger STOP
Danger zone, do not proceed
:::
```
:::danger STOP
Danger zone, do not proceed
:::

#### Output

Expand Down Expand Up @@ -455,9 +414,11 @@ HTML or XML fragments can also be copied by escaping them first:

#### Input

:::copy
`<PackageReference Include="ServiceStack" Version="8.*" />`
:::
```md
:::copy
`<PackageReference Include="ServiceStack" Version="8.*" />`
:::
```

#### Output

Expand Down Expand Up @@ -492,3 +453,47 @@ For embedding YouTube Videos, optimally rendered using the `<LiteYouTube>` compo
#### Output

:::youtube YIa0w6whe2U:::

## Markdown Fenced Code Blocks

For more flexibility you can utilize custom fenced components like the `files` fenced code block which can
be used to capture ascii representation of a structured documentation like a folder & file structure, e.g:

```files
/_videos
/vue
admin.md
autoquerygrid.md
components.md
/react
locode.md
bookings.md
nextjs.md
```

That we can render into a more UX-friendly representation by calling the `Files` component with the body
of the code-block to convert the structured ascii layout into a more familiar GUI layout:

```files
/_videos
/vue
admin.md
autoquerygrid.md
components.md
/react
locode.md
bookings.md
nextjs.md
```

This benefit of this approach of marking up documentation is that the markdown content still remains in an optimal
human-readable form even when the markdown renderer lacks the custom fenced components to render the richer UI.

## Components In Markdown

Up till now all above features will let you render the same markdown content in all available Vue, React, Razor or Blazor
templates. At the cost of reduced portability, you're also able to embed rich Interactive Vue or React components directly in
markdown.

:::include component-links.md:::

Empty file.
1 change: 1 addition & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare module 'vue' {
BlogPosts: typeof import('./components/BlogPosts.vue')['default']
BlogTitle: typeof import('./components/BlogTitle.vue')['default']
FileLayout: typeof import('./components/FileLayout.vue')['default']
Files: typeof import('./components/Files.vue')['default']
FollowLinks: typeof import('./components/FollowLinks.vue')['default']
GettingStarted: typeof import('./components/GettingStarted.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
Expand Down
6 changes: 3 additions & 3 deletions src/components/FileLayout.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<template>
<div v-if="files">
<FileLayout v-for="(contents,label) in files" :label="label" :contents="contents" />
<FileLayout v-for="(contents,label) in files" :key="label" :label="label" :contents="contents" />
</div>
<div v-else>
<div v-if="label == '_'">
<div v-for="file in contents" class="ml-6 flex items-center text-base leading-8">
<div v-for="file in contents" :key="file" class="ml-6 flex items-center text-base leading-8">
<svg class="mr-1 text-slate-600" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"></path></svg>
<span>{{file}}</span>
</div>
Expand All @@ -16,7 +16,7 @@
<svg class="mr-1 text-sky-500" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"><path d="M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"></path></svg>
<span>{{label}}</span>
</div>
<FileLayout v-for="(children,item) in contents" :label="item" :contents="children" />
<FileLayout v-for="(children,item) in contents" :key="item" :label="item" :contents="children" />
</div>
</div>
</div>
Expand Down
67 changes: 67 additions & 0 deletions src/components/Files.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<FileLayout :files="files" />
</template>

<script setup lang="ts">
import FileLayout from '@/components/FileLayout.vue'
const props = defineProps<{
body: string
}>()
/* Takes an ascii string of indented folder and file paths:
const from = `/meta
/2022
all.json
posts.json
videos.json
/2023
all.json
posts.json
all.json
index.json`
// and returns a nested object representing the file structure:
const to = {
meta: {
2022: { _: ['all.json','posts.json','videos.json'] },
2023: { _: ['all.json','posts.json'] },
_: ['all.json','index.json']
}
}
*/
function parseFileStructure(ascii: string) {
const parseLineIndentation = (line:string) => {
const match = line.match(/^(\s*)/)
return match ? match[0].length / 2 : 0
}
const lines = ascii.trim().split("\n")
const root = {}
let currentPath: any = [root]
lines.forEach((line) => {
const name = line.trim()
const isFile = name.includes(".")
const level = parseLineIndentation(line)
// Navigate up the currentPath to find the current level's parent
while (level < currentPath.length - 1) {
currentPath.pop()
}
if (isFile) {
// Current Line is a file, add it to the files array (denoted by "_")
currentPath[level]._ ??= []
currentPath[level]._.push(name)
} else {
const dir = name.replace("/", "")
// Current Line is a folder, create a new object and update the currentPath
currentPath[level][dir] ??= {}
currentPath.push(currentPath[level][dir])
}
})
return root
}
const txt = props.body?.trim() || ''
const files = parseFileStructure(txt)
</script>
Loading

0 comments on commit a918ce1

Please sign in to comment.