Add YAML-metadata for notes and change the document title accordingly (#310)

* Added yaml-frontmatter extracting and error handling
* add tests
* changed document-title, so the editor can change the title to the title of the yaml metadata. closes #303
* extracted first line parsing in a core rule of markdown-it
document title will now be determined like this:
1. yaml metadata title
2. opengraph title
3. first level one heading
4. 'Untitled'
* added documentTitle e2e test

Co-authored-by: Erik Michelson <github@erik.michelson.eu>
Co-authored-by: Philip Molares <philip@mauricedoepke.de>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: mrdrogdrog <mr.drogdrog@gmail.com>
This commit is contained in:
Philip Molares 2020-07-18 22:17:36 +02:00 committed by GitHub
parent 07fed5c67e
commit 29709d2ba4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 499 additions and 20 deletions

View file

@ -27,7 +27,7 @@ jobs:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: yarn install
- name: run unit tests
- name: Test Project
run: yarn test
- name: Build project
run: yarn build

View file

@ -0,0 +1,67 @@
import { branding } from '../support/config'
const title = 'This is a test title'
describe('Document Title', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
.should('exist')
cy.get('.CodeMirror textarea')
.type('{ctrl}a', { force: true })
.type('{backspace}')
})
describe('title should be yaml metadata title', () => {
it('just yaml metadata title', () => {
cy.get('.CodeMirror textarea')
.type(`---\ntitle: ${title}\n---`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
it('yaml metadata title and opengraph title', () => {
cy.get('.CodeMirror textarea')
.type(`---\ntitle: ${title}\nopengraph:\n title: False title\n{backspace}{backspace}---`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
it('yaml metadata title, opengraph title and first heading', () => {
cy.get('.CodeMirror textarea')
.type(`---\ntitle: ${title}\nopengraph:\n title: False title\n{backspace}{backspace}---\n# a first title`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
})
describe('title should be opengraph title', () => {
it('just opengraph title', () => {
cy.get('.CodeMirror textarea')
.type(`---\nopengraph:\n title: ${title}\n{backspace}{backspace}---`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
it('opengraph title and first heading', () => {
cy.get('.CodeMirror textarea')
.type(`---\nopengraph:\n title: ${title}\n{backspace}{backspace}---\n# a first title`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
})
describe('title should be first heading', () => {
it('just first heading', () => {
cy.get('.CodeMirror textarea')
.type(`# ${title}`)
cy.title().should('eq', `${title} - CodiMD @ ${branding.name}`)
})
it('just first heading with alt-text instead of image', () => {
cy.get('.CodeMirror textarea')
.type(`# ${title} ![abc](https://dummyimage.com/48)`)
cy.title().should('eq', `${title} abc - CodiMD @ ${branding.name}`)
})
it('just first heading without link syntax', () => {
cy.get('.CodeMirror textarea')
.type(`# ${title} [link](https://hedgedoc.org)`)
cy.title().should('eq', `${title} link - CodiMD @ ${branding.name}`)
})
})
})

View file

@ -3,6 +3,11 @@ export const banner = {
timestamp: '2020-05-22T20:46:08.962Z'
}
export const branding = {
name: 'ACME Corp',
logo: 'http://localhost:3000/acme.png'
}
beforeEach(() => {
cy.server()
cy.route({
@ -22,10 +27,7 @@ beforeEach(() => {
email: true,
openid: true
},
branding: {
name: 'ACME Corp',
logo: 'http://localhost:3000/acme.png'
},
branding: branding,
banner: banner,
customAuthNames: {
ldap: 'FooBar',

View file

@ -10,6 +10,7 @@
"@types/deep-equal": "1.0.1",
"@types/highlight.js": "9.12.4",
"@types/jest": "26.0.4",
"@types/js-yaml": "3.12.5",
"@types/markdown-it": "10.0.1",
"@types/markdown-it-anchor": "4.0.4",
"@types/markdown-it-container": "2.0.3",
@ -44,6 +45,7 @@
"i18next": "19.6.2",
"i18next-browser-languagedetector": "5.0.0",
"i18next-http-backend": "1.0.17",
"js-yaml": "^3.14.0",
"markdown-it": "11.0.0",
"markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.3.0",
@ -51,6 +53,7 @@
"markdown-it-deflist": "2.0.3",
"markdown-it-emoji": "1.4.0",
"markdown-it-footnote": "3.0.2",
"markdown-it-front-matter": "0.2.1",
"markdown-it-imsize": "2.0.1",
"markdown-it-ins": "3.0.0",
"markdown-it-mark": "3.0.0",

View file

@ -139,7 +139,9 @@
}
},
"editor": {
"untitledNote": "Untitled",
"placeholder": "← Start by entering a title here\n===\nVisit the features page if you don't know what to do.\nHappy hacking :)",
"invalidYaml": "The yaml-header is invalid. See <0></0> for more information.",
"help": {
"contacts": {
"title": "Contacts",

View file

@ -2,12 +2,16 @@ import React, { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
export const DocumentTitle: React.FC = () => {
export interface DocumentTitleProps {
title?: string
}
export const DocumentTitle: React.FC<DocumentTitleProps> = ({ title }) => {
const branding = useSelector((state: ApplicationState) => state.backendConfig.branding)
useEffect(() => {
document.title = `CodiMD ${branding.name ? ` @ ${branding.name}` : ''}`
}, [branding])
document.title = `${title ? title + ' - ' : ''}CodiMD ${branding.name ? ` @ ${branding.name}` : ''}`
}, [branding, title])
return null
}

View file

@ -1,18 +1,31 @@
import React, { Fragment, useEffect, useState } from 'react'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import useMedia from 'use-media'
import { ApplicationState } from '../../redux'
import { setEditorModeConfig } from '../../redux/editor/methods'
import { DocumentTitle } from '../common/document-title/document-title'
import { Splitter } from '../common/splitter/splitter'
import { InfoBanner } from '../landing/layout/info-banner'
import { EditorWindow } from './editor-window/editor-window'
import { MarkdownRenderWindow } from './renderer-window/markdown-render-window'
import { EditorMode } from './task-bar/editor-view-mode'
import { TaskBar } from './task-bar/task-bar'
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
const Editor: React.FC = () => {
export const Editor: React.FC = () => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const [markdownContent, setMarkdownContent] = useState(`# Embedding demo
const [markdownContent, setMarkdownContent] = useState(`---
title: Features
description: Many features, such wow!
robots: noindex
tags: codimd, demo, react
opengraph:
title: Features
---
# Embedding demo
[TOC]
## MathJax
@ -55,12 +68,36 @@ https://asciinema.org/a/117928
## Code highlighting
\`\`\`javascript=
let a = 1
\`\`\`
`)
const isWide = useMedia({ minWidth: 576 })
const [firstDraw, setFirstDraw] = useState(true)
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const noteMetadata = useRef<YAMLMetaData>()
const firstHeading = useRef<string>()
const updateDocumentTitle = useCallback(() => {
if (noteMetadata.current?.title && noteMetadata.current?.title !== '') {
setDocumentTitle(noteMetadata.current.title)
} else if (noteMetadata.current?.opengraph && noteMetadata.current?.opengraph.get('title') && noteMetadata.current?.opengraph.get('title') !== '') {
setDocumentTitle(noteMetadata.current.opengraph.get('title') ?? untitledNote)
} else {
setDocumentTitle(firstHeading.current ?? untitledNote)
}
}, [untitledNote])
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
noteMetadata.current = metaData
updateDocumentTitle()
}, [updateDocumentTitle])
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
firstHeading.current = newFirstHeading
updateDocumentTitle()
}, [updateDocumentTitle])
useEffect(() => {
setFirstDraw(false)
@ -75,17 +112,16 @@ let a = 1
return (
<Fragment>
<InfoBanner/>
<DocumentTitle title={documentTitle}/>
<div className={'d-flex flex-column vh-100'}>
<TaskBar/>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={<EditorWindow onContentChange={content => setMarkdownContent(content)} content={markdownContent}/>}
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
right={<MarkdownRenderWindow content={markdownContent} wide={editorMode === EditorMode.PREVIEW}/>}
right={<MarkdownRenderWindow content={markdownContent} wide={editorMode === EditorMode.PREVIEW} onMetadataChange={onMetadataChange} onFirstHeadingChange={onFirstHeadingChange}/>}
containerClassName={'overflow-hidden'}/>
</div>
</Fragment>
)
}
export { Editor }

View file

@ -1,5 +1,6 @@
import equal from 'deep-equal'
import { DomElement } from 'domhandler'
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import anchor from 'markdown-it-anchor'
@ -7,6 +8,7 @@ import markdownItContainer from 'markdown-it-container'
import definitionList from 'markdown-it-deflist'
import emoji from 'markdown-it-emoji'
import footnote from 'markdown-it-footnote'
import frontmatter from 'markdown-it-front-matter'
import imsize from 'markdown-it-imsize'
import inserted from 'markdown-it-ins'
import marked from 'markdown-it-mark'
@ -16,11 +18,16 @@ import subscript from 'markdown-it-sub'
import superscript from 'markdown-it-sup'
import taskList from 'markdown-it-task-lists'
import toc from 'markdown-it-toc-done-right'
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
import { Trans } from 'react-i18next'
import MathJaxReact from 'react-mathjax'
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
import { slugify } from '../../../utils/slugify'
import { InternalLink } from '../../common/links/internal-link'
import { ShowIf } from '../../common/show-if/show-if'
import { RawYAMLMetadata, YAMLMetaData } from '../yaml-metadata/yaml-metadata'
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
@ -57,11 +64,34 @@ export interface MarkdownRendererProps {
wide?: boolean
className?: string
onTocChange?: (ast: TocAst) => void
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
onFirstHeadingChange?: (firstHeading: string | undefined) => void
}
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className, onTocChange, wide }) => {
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
const [tocAst, setTocAst] = useState<TocAst>()
const [lastTocAst, setLastTocAst] = useState<TocAst>()
const [yamlError, setYamlError] = useState(false)
const rawMetaRef = useRef<RawYAMLMetadata>()
const oldMetaRef = useRef<RawYAMLMetadata>()
const firstHeadingRef = useRef<string>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
if (rawMetaRef.current) {
const newMetaData = new YAMLMetaData(rawMetaRef.current)
onMetaDataChange(newMetaData)
} else {
onMetaDataChange(undefined)
}
oldMetaRef.current = rawMetaRef.current
}
if (onFirstHeadingChange && !equal(firstHeadingRef.current, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef.current || undefined)
oldFirstHeadingRef.current = firstHeadingRef.current
}
})
const markdownIt = useMemo(() => {
const md = new MarkdownIt('default', {
@ -70,6 +100,32 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
langPrefix: '',
typographer: true
})
if (onFirstHeadingChange) {
md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
const lines = state.src.split('\n')
const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
for (const line of lines) {
if (line.startsWith('# ')) {
firstHeadingRef.current = line.replace('# ', '').replace(linkAltTextRegex, '$1')
return true
}
}
firstHeadingRef.current = undefined
return true
})
}
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
}
})
}
md.use(taskList)
md.use(emoji)
md.use(abbreviation)
@ -79,6 +135,19 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
md.use(inserted)
md.use(marked)
md.use(footnote)
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
rawMetaRef.current = ({} as RawYAMLMetadata)
}
})
}
md.use(imsize)
// noinspection CheckTagEmptyBody
md.use(anchor, {
@ -126,7 +195,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
})
return md
}, [])
}, [onMetaDataChange, onFirstHeadingChange])
useEffect(() => {
if (onTocChange && tocAst && !equal(tocAst, lastTocAst)) {
@ -155,6 +224,10 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
new QuoteOptionsReplacer(),
new MathjaxReplacer()
]
if (onMetaDataChange) {
// This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
rawMetaRef.current = undefined
}
const html: string = markdownIt.render(content)
const transform: Transform = (node, index) => {
@ -162,10 +235,17 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
return tryToReplaceNode(node, index, allReplacers, subNodeConverter) || convertNodeToElement(node, index, transform)
}
return ReactHtmlParser(html, { transform: transform })
}, [content, markdownIt])
}, [content, markdownIt, onMetaDataChange])
return (
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
<ShowIf condition={yamlError}>
<Alert variant='warning' dir='auto'>
<Trans i18nKey='editor.invalidYaml'>
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
</Trans>
</Alert>
</ShowIf>
<MathJaxReact.Provider>
{result}
</MathJaxReact.Provider>

View file

@ -6,13 +6,16 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
import { MarkdownRenderer } from '../markdown-renderer/markdown-renderer'
import { MarkdownToc } from '../markdown-toc/markdown-toc'
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
interface RenderWindowProps {
content: string
onMetadataChange: (metaData: YAMLMetaData | undefined) => void
onFirstHeadingChange: (firstHeading: string | undefined) => void
wide?: boolean
}
export const MarkdownRenderWindow: React.FC<RenderWindowProps> = ({ content, wide }) => {
export const MarkdownRenderWindow: React.FC<RenderWindowProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide }) => {
const [tocAst, setTocAst] = useState<TocAst>()
const renderer = useRef<HTMLDivElement>(null)
const { width } = useResizeObserver({ ref: renderer })
@ -26,7 +29,10 @@ export const MarkdownRenderWindow: React.FC<RenderWindowProps> = ({ content, wid
className={'flex-fill'}
content={content}
wide={wide}
onTocChange={(tocAst) => setTocAst(tocAst)}/>
onTocChange={(tocAst) => setTocAst(tocAst)}
onMetaDataChange={onMetadataChange}
onFirstHeadingChange={onFirstHeadingChange}
/>
<div className={`col-md d-flex flex-column ${realWidth < 1280 ? 'justify-content-end' : ''}`}>
<ShowIf condition={realWidth >= 1280 && !!tocAst}>

View file

@ -0,0 +1,203 @@
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata, YAMLMetaData } from './yaml-metadata'
describe('yaml tests', () => {
let raw: RawYAMLMetadata | undefined
let finished: YAMLMetaData | undefined
const md = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
typographer: true
})
md.use(frontmatter, (rawMeta: string) => {
raw = yaml.safeLoad(rawMeta) as RawYAMLMetadata
finished = new YAMLMetaData(raw)
})
// generate default YAMLMetadata
md.render('---\n---')
const defaultYAML = finished
const testMetadata = (input: string, expectedRaw: Partial<RawYAMLMetadata>, expectedFinished: Partial<YAMLMetaData>) => {
md.render(input)
expect(raw).not.toBe(undefined)
expect(raw).toEqual(expectedRaw)
expect(finished).not.toBe(undefined)
expect(finished).toEqual({
...defaultYAML,
...expectedFinished
})
}
beforeEach(() => {
raw = undefined
finished = undefined
})
it('title only', () => {
testMetadata(`---
title: test
___
`,
{
title: 'test'
},
{
title: 'test'
})
})
it('robots only', () => {
testMetadata(`---
robots: index, follow
___
`,
{
robots: 'index, follow'
},
{
robots: 'index, follow'
})
})
it('tags only', () => {
testMetadata(`---
tags: test123, abc
___
`,
{
tags: 'test123, abc'
},
{
tags: ['test123', 'abc']
})
})
it('breaks only', () => {
testMetadata(`---
breaks: false
___
`,
{
breaks: false
},
{
breaks: false
})
})
/*
it('slideOptions nothing', () => {
testMetadata(`---
slideOptions:
___
`,
{
slideOptions: null
},
{
slideOptions: {
theme: 'white',
transition: 'none'
}
})
})
it('slideOptions.theme only', () => {
testMetadata(`---
slideOptions:
theme: sky
___
`,
{
slideOptions: {
theme: 'sky',
transition: undefined
}
},
{
slideOptions: {
theme: 'sky',
transition: 'none'
}
})
})
it('slideOptions full', () => {
testMetadata(`---
slideOptions:
transition: zoom
theme: sky
___
`,
{
slideOptions: {
theme: 'sky',
transition: 'zoom'
}
},
{
slideOptions: {
theme: 'sky',
transition: 'zoom'
}
})
})
*/
it('opengraph nothing', () => {
testMetadata(`---
opengraph:
___
`,
{
opengraph: null
},
{
opengraph: new Map<string, string>()
})
})
it('opengraph title only', () => {
testMetadata(`---
opengraph:
title: Testtitle
___
`,
{
opengraph: {
title: 'Testtitle'
}
},
{
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
})
})
it('opengraph more attributes', () => {
testMetadata(`---
opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png
___
`,
{
opengraph: {
title: 'Testtitle',
image: 'https://dummyimage.com/48.png',
'image:type': 'image/png'
}
},
{
opengraph: new Map<string, string>(Object.entries({
title: 'Testtitle',
image: 'https://dummyimage.com/48.png',
'image:type': 'image/png'
}))
})
})
})

View file

@ -0,0 +1,53 @@
// import { RevealOptions } from 'reveal.js'
type iso6391 = 'aa' | 'ab' | 'af' | 'am' | 'ar' | 'ar-ae' | 'ar-bh' | 'ar-dz' | 'ar-eg' | 'ar-iq' | 'ar-jo' | 'ar-kw' | 'ar-lb' | 'ar-ly' | 'ar-ma' | 'ar-om' | 'ar-qa' | 'ar-sa' | 'ar-sy' | 'ar-tn' | 'ar-ye' | 'as' | 'ay' | 'de-at' | 'de-ch' | 'de-li' | 'de-lu' | 'div' | 'dz' | 'el' | 'en' | 'en-au' | 'en-bz' | 'en-ca' | 'en-gb' | 'en-ie' | 'en-jm' | 'en-nz' | 'en-ph' | 'en-tt' | 'en-us' | 'en-za' | 'en-zw' | 'eo' | 'es' | 'es-ar' | 'es-bo' | 'es-cl' | 'es-co' | 'es-cr' | 'es-do' | 'es-ec' | 'es-es' | 'es-gt' | 'es-hn' | 'es-mx' | 'es-ni' | 'es-pa' | 'es-pe' | 'es-pr' | 'es-py' | 'es-sv' | 'es-us' | 'es-uy' | 'es-ve' | 'et' | 'eu' | 'fa' | 'fi' | 'fj' | 'fo' | 'fr' | 'fr-be' | 'fr-ca' | 'fr-ch' | 'fr-lu' | 'fr-mc' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'ha' | 'he' | 'hi' | 'hr' | 'hu' | 'hy' | 'ia' | 'id' | 'ie' | 'ik' | 'in' | 'is' | 'it' | 'it-ch' | 'iw' | 'ja' | 'ji' | 'jw' | 'ka' | 'kk' | 'kl' | 'km' | 'kn' | 'ko' | 'kok' | 'ks' | 'ku' | 'ky' | 'kz' | 'la' | 'ln' | 'lo' | 'ls' | 'lt' | 'lv' | 'mg' | 'mi' | 'mk' | 'ml' | 'mn' | 'mo' | 'mr' | 'ms' | 'mt' | 'my' | 'na' | 'nb-no' | 'ne' | 'nl' | 'nl-be' | 'nn-no' | 'no' | 'oc' | 'om' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'pt-br' | 'qu' | 'rm' | 'rn' | 'ro' | 'ro-md' | 'ru' | 'ru-md' | 'rw' | 'sa' | 'sb' | 'sd' | 'sg' | 'sh' | 'si' | 'sk' | 'sl' | 'sm' | 'sn' | 'so' | 'sq' | 'sr' | 'ss' | 'st' | 'su' | 'sv' | 'sv-fi' | 'sw' | 'sx' | 'syr' | 'ta' | 'te' | 'tg' | 'th' | 'ti' | 'tk' | 'tl' | 'tn' | 'to' | 'tr' | 'ts' | 'tt' | 'tw' | 'uk' | 'ur' | 'us' | 'uz' | 'vi' | 'vo' | 'wo' | 'xh' | 'yi' | 'yo' | 'zh' | 'zh-cn' | 'zh-hk' | 'zh-mo' | 'zh-sg' | 'zh-tw' | 'zu'
export interface RawYAMLMetadata {
title: string | undefined
description: string | undefined
tags: string | undefined
robots: string | undefined
lang: string | undefined
dir: string | undefined
breaks: boolean | undefined
GA: string | undefined
disqus: string | undefined
type: string | undefined
slideOptions: any
opengraph: any
}
export class YAMLMetaData {
title: string
description: string
tags: string[]
robots: string
lang: iso6391
dir: 'ltr' | 'rtl'
breaks: boolean
GA: string
disqus: string
type: 'slide' | ''
// slideOptions: RevealOptions
opengraph: Map<string, string>
constructor (rawData: RawYAMLMetadata) {
this.title = rawData?.title ?? ''
this.description = rawData?.description ?? ''
this.robots = rawData?.robots ?? ''
this.breaks = rawData?.breaks ?? true
this.GA = rawData?.GA ?? ''
this.disqus = rawData?.disqus ?? ''
this.type = (rawData?.type as YAMLMetaData['type']) ?? ''
this.lang = (rawData?.lang as iso6391) ?? 'en'
this.dir = (rawData?.dir as YAMLMetaData['dir']) ?? 'ltr'
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
transition: 'none',
theme: 'white'
} */
this.tags = rawData?.tags?.split(',').map(entry => entry.trim()) ?? []
this.opengraph = rawData?.opengraph ? new Map<string, string>(Object.entries(rawData.opengraph)) : new Map<string, string>()
}
}

View file

@ -0,0 +1,5 @@
declare module 'markdown-it-front-matter' {
import MarkdownIt from 'markdown-it/lib'
const markdownItFrontMatter: MarkdownIt.PluginSimple
export = markdownItFrontMatter
}

View file

@ -1700,6 +1700,11 @@
jest-diff "^25.2.1"
pretty-format "^25.2.1"
"@types/js-yaml@3.12.5":
version "3.12.5"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb"
integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==
"@types/json-schema@^7.0.3":
version "7.0.4"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
@ -7399,6 +7404,14 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.14.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -8027,6 +8040,11 @@ markdown-it-footnote@3.0.2:
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz#1575ee7a093648d4e096aa33386b058d92ac8bc1"
integrity sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A==
markdown-it-front-matter@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.1.tgz#dca49a827bb3cebb0528452c1d87dff276eb28dc"
integrity sha512-ydUIqlKfDscRpRUTRcA3maeeUKn3Cl5EaKZSA+I/f0KOGCBurW7e+bbz59sxqkC3FA9Q2S2+t4mpkH9T0BCM6A==
markdown-it-imsize@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz#cca0427905d05338a247cb9ca9d968c5cddd5170"