feat: add changelog page
This commit is contained in:
108
one/build/changelog.js
Normal file
108
one/build/changelog.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import cheerio from 'cheerio'
|
||||
import { render } from './page'
|
||||
|
||||
const VERSION_RE = /^(\d+\.\d+\.\d+(?:-[a-z]+(?:\.\d+)?)?)(?:\s+"([^"]+)")?$/i
|
||||
function getVersion (title = '') {
|
||||
const [, version, codeName] = title.trim().match(VERSION_RE) || []
|
||||
if (!version) {
|
||||
return null
|
||||
}
|
||||
return [version, codeName]
|
||||
}
|
||||
|
||||
const TYPE_MAP = {
|
||||
'⚠️': 'breaking',
|
||||
'💡': 'feature',
|
||||
'🐞': 'bugfix',
|
||||
'🧪': 'experimental'
|
||||
}
|
||||
const TYPE_KEYS = Object.keys(TYPE_MAP)
|
||||
function getChangeType (title) {
|
||||
const t = title.trim()
|
||||
const key = TYPE_KEYS.find(key => t.includes(key))
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
return TYPE_MAP[key]
|
||||
}
|
||||
|
||||
const TAG_RE = /#([^\s]+)$/
|
||||
function getTags (comment) {
|
||||
return comment
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(token => {
|
||||
const [, tag] = token.match(TAG_RE) || []
|
||||
return tag
|
||||
})
|
||||
.filter(tag => !!tag)
|
||||
}
|
||||
|
||||
function extract (html) {
|
||||
const changelog = []
|
||||
|
||||
const $ = cheerio.load(html)
|
||||
const $versions = $('h2')
|
||||
|
||||
$versions.each((_, el) => {
|
||||
const $version = $(el)
|
||||
const [version, codeName] = getVersion($(el).text()) || []
|
||||
const versionLog = {
|
||||
version,
|
||||
codeName,
|
||||
changeset: []
|
||||
}
|
||||
|
||||
const $types = $version.nextUntil('h2', 'h3')
|
||||
|
||||
if ($types.length === 0) {
|
||||
throw new Error(`No change type found for version ${version}`)
|
||||
}
|
||||
|
||||
let type
|
||||
$types.each((_, el) => {
|
||||
const $type = $(el)
|
||||
type = getChangeType($type.text())
|
||||
|
||||
const $changeset = $type.next('ul').children()
|
||||
|
||||
if ($changeset.length === 0) {
|
||||
throw new Error(`No changeset found for version ${version}`)
|
||||
}
|
||||
|
||||
$changeset.each((_, el) => {
|
||||
const $change = $(el)
|
||||
const tags = $change
|
||||
.contents()
|
||||
.toArray()
|
||||
.map(el => {
|
||||
if (el.type === 'comment') {
|
||||
return getTags(el.data)
|
||||
}
|
||||
return []
|
||||
})
|
||||
.reduce((all, current) => all.concat(current), [])
|
||||
|
||||
$change.contents().filter((_, el) => el.type === 'comment').remove()
|
||||
|
||||
versionLog.changeset.push({
|
||||
type,
|
||||
tags,
|
||||
content: $change.html().replace(/^\n+|\n+$/g, '')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
changelog.push(versionLog)
|
||||
})
|
||||
|
||||
return changelog
|
||||
}
|
||||
|
||||
export function getChangelogData () {
|
||||
const changelogPath = require.resolve('veui/CHANGELOG.md')
|
||||
const raw = readFileSync(changelogPath, 'utf8')
|
||||
const { contents } = render(raw, changelogPath)
|
||||
return extract(contents)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { statSync } from 'fs'
|
||||
import { statSync, writeFileSync } from 'fs'
|
||||
import { resolve, relative, extname, basename, sep } from 'path'
|
||||
import readdirpSync from 'recursive-readdir-sync'
|
||||
import rimraf from 'rimraf'
|
||||
import { copyFileSync, replaceExtSync } from './util'
|
||||
import { renderDocToPage } from './page'
|
||||
import { get, removeFile } from './deps'
|
||||
import { getChangelogData } from './changelog'
|
||||
|
||||
const DOCS_DIR = resolve(__dirname, '../docs')
|
||||
const PAGES_DIR = resolve(__dirname, '../../pages')
|
||||
@@ -18,17 +19,28 @@ export function generatePages (file, stats) {
|
||||
rimraf.sync(resolve(__dirname, './deps.json'))
|
||||
console.log('Regenerating all files...')
|
||||
handleFile(DOCS_DIR)
|
||||
handleChangelog()
|
||||
console.log('...done.')
|
||||
} else {
|
||||
handleFile(file, stats)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangelog () {
|
||||
const changelogData = getChangelogData()
|
||||
writeFileSync(
|
||||
resolve(ASSETS_DIR, 'data', 'changelog.json'),
|
||||
JSON.stringify(changelogData, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
function handleFile (file, stats) {
|
||||
let segments = relative(DOCS_DIR, file).split(sep)
|
||||
if (segments.some(segment => {
|
||||
return segment.startsWith('_') || segment.startsWith('.')
|
||||
})) {
|
||||
if (
|
||||
segments.some(segment => {
|
||||
return segment.startsWith('_') || segment.startsWith('.')
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,9 +86,10 @@ function handleFile (file, stats) {
|
||||
default: {
|
||||
let relDest = relative(DOCS_DIR, file)
|
||||
|
||||
let dest = relDest.split(sep).indexOf('demo') === -1
|
||||
? resolve(PAGES_DIR, relDest)
|
||||
: resolve(DEMOS_DIR, relDest)
|
||||
let dest =
|
||||
relDest.split(sep).indexOf('demo') === -1
|
||||
? resolve(PAGES_DIR, relDest)
|
||||
: resolve(DEMOS_DIR, relDest)
|
||||
if (remove) {
|
||||
rimraf.sync(dest)
|
||||
console.log(`[${relDest}] removed.`)
|
||||
|
||||
@@ -3,11 +3,11 @@ import vfile from 'vfile'
|
||||
import remark from 'remark'
|
||||
import slug from 'remark-slug'
|
||||
import frontmatter from 'remark-frontmatter'
|
||||
import highlight from 'remark-highlight.js'
|
||||
import shortcodes from 'remark-shortcodes'
|
||||
import remarkToRehype from 'remark-rehype'
|
||||
import raw from 'rehype-raw'
|
||||
import html from 'rehype-stringify'
|
||||
import highlight from 'rehype-highlight'
|
||||
import etpl from 'etpl'
|
||||
import { readFileSync, writeFileSync, replaceExtSync } from './util'
|
||||
import demo from './remark-demo'
|
||||
@@ -17,9 +17,10 @@ import custom from './remark-custom'
|
||||
import extractFrontmatter from './remark-extract-frontmatter'
|
||||
import rehypePreviewImg from './rehype-preview-img'
|
||||
import rehypeLink from './rehype-link'
|
||||
import rehypeScoped from './rehype-scoped'
|
||||
import rehypeDemo from './rehype-demo'
|
||||
import rehypePre from './rehype-pre'
|
||||
import { add } from './deps'
|
||||
import lowlight from 'lowlight'
|
||||
import { vue } from './language'
|
||||
|
||||
const DOCS_DIR = resolve(__dirname, '../docs')
|
||||
@@ -28,8 +29,6 @@ const PAGE_TPL = readFileSync(resolve(__dirname, '../templates/page.etpl'))
|
||||
|
||||
const renderPage = etpl.compile(PAGE_TPL)
|
||||
|
||||
lowlight.registerLanguage('vue', vue)
|
||||
|
||||
const md = remark()
|
||||
.use(custom)
|
||||
.use(details)
|
||||
@@ -38,13 +37,15 @@ const md = remark()
|
||||
.use(shortcodes)
|
||||
.use(demo)
|
||||
.use(extractFrontmatter)
|
||||
.use(highlight)
|
||||
.use(slug)
|
||||
.use(remarkToRehype, { allowDangerousHTML: true })
|
||||
.use(raw)
|
||||
.use(rehypePreviewImg)
|
||||
.use(rehypeLink)
|
||||
.use(rehypeScoped)
|
||||
.use(rehypeDemo)
|
||||
.use(highlight, { languages: { vue } })
|
||||
.use(rehypePre)
|
||||
.use(html, { allowDangerousHTML: true })
|
||||
|
||||
export function render (contents, path, data = {}) {
|
||||
@@ -59,7 +60,13 @@ export function renderDocToPage (file) {
|
||||
let src = resolve(DOCS_DIR, file)
|
||||
let dest = resolve(PAGES_DIR, replaceExtSync(file, 'vue'))
|
||||
let { contents, data } = renderFile(src, dest)
|
||||
let { demos = {}, components = {}, meta = {}, deps = {}, hasAlert = false } = data
|
||||
let {
|
||||
demos = {},
|
||||
components = {},
|
||||
meta = {},
|
||||
deps = {},
|
||||
hasAlert = false
|
||||
} = data
|
||||
|
||||
Object.keys(deps || {}).forEach(dep => {
|
||||
add({ [dep]: { [src]: true } })
|
||||
@@ -69,7 +76,12 @@ export function renderDocToPage (file) {
|
||||
let componentList = Object.keys(components)
|
||||
let demoList = Object.keys(demos)
|
||||
let result = renderPage({
|
||||
content: (contents || '').replace(/\n{3,}/g, '\n\n'),
|
||||
content: (contents || '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.replace(/\{/g, '{')
|
||||
.replace(/\}/g, '}')
|
||||
.replace(/v-pre="true"/g, 'v-pre')
|
||||
.replace(/data-markdown="true"/g, 'data-markdown'),
|
||||
demos: demoList.map(name => {
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -24,16 +24,10 @@ export default function attacher () {
|
||||
{
|
||||
slot: 'source'
|
||||
},
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
'v-pre': true
|
||||
},
|
||||
{
|
||||
type: 'raw',
|
||||
value: code
|
||||
}
|
||||
)
|
||||
{
|
||||
type: 'raw',
|
||||
value: code
|
||||
}
|
||||
),
|
||||
h(
|
||||
'template',
|
||||
|
||||
11
one/build/rehype-pre.js
Normal file
11
one/build/rehype-pre.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import visit from 'unist-util-visit'
|
||||
|
||||
export default function attacher () {
|
||||
return tree => {
|
||||
visit(tree, 'element', ({ tagName, properties }) => {
|
||||
if (tagName === 'pre') {
|
||||
properties['v-pre'] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
13
one/build/rehype-scoped.js
Normal file
13
one/build/rehype-scoped.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import visit from 'unist-util-visit'
|
||||
|
||||
const RE_DEMO = /^one-demo-[a-f0-9]+/i
|
||||
|
||||
export default function attacher () {
|
||||
return tree => {
|
||||
visit(tree, 'element', ({ tagName, properties }, _, { type }) => {
|
||||
if (type === 'root' && !RE_DEMO.test(tagName)) {
|
||||
properties['data-markdown'] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import tokenizer from './customBlock'
|
||||
import tokenizer from './custom-block'
|
||||
import visit from 'unist-util-visit'
|
||||
import { render } from './page'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import tokenizer from './refBlock'
|
||||
import tokenizer from './ref-block'
|
||||
import visit from 'unist-util-visit'
|
||||
import remove from 'unist-util-remove'
|
||||
import { render } from './page'
|
||||
@@ -35,7 +35,7 @@ export default function attacher () {
|
||||
|
||||
visit(tree, 'linkReference', (node, index, parent) => {
|
||||
let { identifier } = node
|
||||
let [match, id] = identifier.match(RE_REF)
|
||||
let [match, id] = identifier.match(RE_REF) || []
|
||||
if (!match || !id || !data.refs[id]) {
|
||||
return
|
||||
}
|
||||
|
||||
228
one/docs/changelog.vue
Normal file
228
one/docs/changelog.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<article
|
||||
class="content post"
|
||||
:class="{ 'filter-version': compareValid }"
|
||||
>
|
||||
<h1 data-markdown>
|
||||
变更日志
|
||||
</h1>
|
||||
<veui-form
|
||||
style="--dls-field-label-width: 4em"
|
||||
ui="s"
|
||||
class="form"
|
||||
>
|
||||
<veui-field
|
||||
ui="s"
|
||||
label="变更类型"
|
||||
>
|
||||
<veui-checkbox-group
|
||||
v-model="types"
|
||||
class="types"
|
||||
:items="allTypes"
|
||||
>
|
||||
<template #item="{ label, emoji }">
|
||||
<span class="emoji">{{ emoji }}</span> {{ label }}
|
||||
</template>
|
||||
</veui-checkbox-group>
|
||||
</veui-field>
|
||||
<veui-field
|
||||
ui="s"
|
||||
label="功能筛选"
|
||||
>
|
||||
<veui-select
|
||||
v-model="tag"
|
||||
searchable
|
||||
clearable
|
||||
:options="allTags"
|
||||
placeholder="根据组件/指令/插件/模块等进行过滤……"
|
||||
/>
|
||||
</veui-field>
|
||||
<veui-fieldset
|
||||
ui="s"
|
||||
label="版本对比"
|
||||
>
|
||||
<veui-field>
|
||||
<veui-checkbox
|
||||
v-model="compare"
|
||||
class="compare-toggle"
|
||||
>
|
||||
开启
|
||||
</veui-checkbox>
|
||||
</veui-field>
|
||||
<template v-if="compare">
|
||||
<veui-field>
|
||||
<veui-select
|
||||
v-model="from"
|
||||
class="version"
|
||||
:options="allVersions"
|
||||
searchable
|
||||
clearable
|
||||
placeholder="选择起始版本……"
|
||||
/>
|
||||
</veui-field>
|
||||
→
|
||||
<veui-field>
|
||||
<veui-select
|
||||
v-model="to"
|
||||
class="version"
|
||||
:options="allVersions"
|
||||
searchable
|
||||
clearable
|
||||
placeholder="选择目标版本……"
|
||||
/>
|
||||
</veui-field>
|
||||
</template>
|
||||
</veui-fieldset>
|
||||
</veui-form>
|
||||
<section
|
||||
v-for="{ version, codeName, changeset } of filteredChangelog"
|
||||
:key="version"
|
||||
data-markdown
|
||||
>
|
||||
<h2
|
||||
:class="{
|
||||
major: isMajor(version),
|
||||
minor: isMinor(version),
|
||||
}"
|
||||
>
|
||||
{{ version }} <small>{{ codeName }}</small>
|
||||
</h2>
|
||||
<ul>
|
||||
<li
|
||||
v-for="({ type, tags, content }, index) of changeset"
|
||||
:key="index"
|
||||
v-html="content"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { Form, Field, Fieldset, CheckboxGroup, Select, Checkbox } from 'veui'
|
||||
import changelog from '../assets/data/changelog.json'
|
||||
|
||||
const allTypes = [
|
||||
{ label: '非兼容性变更', value: 'breaking', emoji: '⚠️' },
|
||||
{ label: '主要变更', value: ' feature', emoji: '💡' },
|
||||
{ label: '问题修复', value: 'bugfix', emoji: '🐞' },
|
||||
{ label: '实验性功能', value: 'experimental', emoji: '🧪' }
|
||||
]
|
||||
|
||||
const allVersions = changelog.map(({ version }) => ({ label: version, value: version }))
|
||||
const allTags = [
|
||||
...new Set(changelog.map(({ changeset }) => changeset.map(({ tags }) => tags).flat()).flat())
|
||||
]
|
||||
.sort()
|
||||
.map(tag => ({ label: tag, value: tag }))
|
||||
|
||||
function isMajor (version) {
|
||||
return /^\d+\.0.0$/.test(version)
|
||||
}
|
||||
|
||||
function isMinor (version) {
|
||||
return /^\d+\.(?:[1-9]|\d{2,}).0$/.test(version)
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'one-changelog',
|
||||
layout: 'default',
|
||||
components: {
|
||||
'veui-form': Form,
|
||||
'veui-field': Field,
|
||||
'veui-fieldset': Fieldset,
|
||||
'veui-checkbox-group': CheckboxGroup,
|
||||
'veui-select': Select,
|
||||
'veui-checkbox': Checkbox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
changelog,
|
||||
allTypes,
|
||||
types: allTypes.map(({ value }) => value),
|
||||
allVersions,
|
||||
allTags,
|
||||
compare: false,
|
||||
tag: null,
|
||||
from: null,
|
||||
to: allVersions[0].value
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
compareValid () {
|
||||
return this.from && this.to
|
||||
},
|
||||
filteredChangelog () {
|
||||
const { changelog, tag, from, to } = this
|
||||
|
||||
let result = cloneDeep(changelog)
|
||||
|
||||
if (from && to) {
|
||||
const fromIndex = result.findIndex(({ version }) => version === from)
|
||||
const toIndex = result.findIndex(({ version }) => version === to)
|
||||
result = result.slice(toIndex, fromIndex)
|
||||
}
|
||||
|
||||
result.forEach((versionLog) => {
|
||||
const { changeset } = versionLog
|
||||
versionLog.changeset = changeset
|
||||
.filter(({ type, tags }) => this.types.includes(type) && (!tag || tags.includes(tag)))
|
||||
})
|
||||
|
||||
return result.filter(({ changeset }) => changeset.length !== 0)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
compare (val) {
|
||||
if (!val) {
|
||||
this.from = this.to = null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isMajor,
|
||||
isMinor
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.emoji
|
||||
font-family "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Android Emoji", EmojiSymbols
|
||||
|
||||
.compare-toggle
|
||||
margin-right 8px
|
||||
|
||||
.version
|
||||
width 160px
|
||||
margin 0 8px
|
||||
|
||||
.veui-field
|
||||
--dls-field-label-width inherit
|
||||
|
||||
.form
|
||||
& >>> .veui-field
|
||||
margin-bottom 12px
|
||||
|
||||
& >>> .veui-field .veui-field-no-label
|
||||
margin-bottom 0
|
||||
|
||||
h2
|
||||
font-size 20px
|
||||
margin 1.2em 0 0.6em
|
||||
|
||||
&.minor
|
||||
font-size 24px
|
||||
|
||||
&.major
|
||||
font-size 28px
|
||||
|
||||
&.minor
|
||||
&.major
|
||||
&::before
|
||||
content "§"
|
||||
|
||||
small
|
||||
font-size 18px
|
||||
</style>
|
||||
@@ -49,6 +49,5 @@ export default {
|
||||
|
||||
section {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"slug": "",
|
||||
"exact": true
|
||||
},
|
||||
{
|
||||
"title": "升级日志",
|
||||
"slug": "changelog"
|
||||
},
|
||||
{
|
||||
"title": "起步",
|
||||
"slug": "getting-started",
|
||||
@@ -397,6 +401,10 @@
|
||||
"slug": "",
|
||||
"exact": true
|
||||
},
|
||||
{
|
||||
"title": "Changelog",
|
||||
"slug": "changelog"
|
||||
},
|
||||
{
|
||||
"title": "Getting started",
|
||||
"slug": "getting-started",
|
||||
@@ -714,6 +722,11 @@
|
||||
"title": "Cascader",
|
||||
"slug": "cascader",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"title": "ConfigProvider",
|
||||
"slug": "config-provider",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -27,6 +27,4 @@ export default {<!-- if: ${layout} -->
|
||||
<!-- /if -->
|
||||
mixins: [htmlAttrs]
|
||||
}
|
||||
</script><!-- /else --><!-- if: ${style} -->
|
||||
|
||||
<style lang="stylus" src="@/assets/styles/post.styl" scoped></style><!-- /if -->
|
||||
</script><!-- /else -->
|
||||
|
||||
Reference in New Issue
Block a user