diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0744e5230..0d1b38a62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/cypress/integration/documentTitle.spec.ts b/cypress/integration/documentTitle.spec.ts new file mode 100644 index 000000000..2494031ff --- /dev/null +++ b/cypress/integration/documentTitle.spec.ts @@ -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}`) + }) + }) +}) diff --git a/cypress/support/config.ts b/cypress/support/config.ts index 0a9ef2872..c9dbbacfd 100644 --- a/cypress/support/config.ts +++ b/cypress/support/config.ts @@ -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', diff --git a/package.json b/package.json index ab5aaac7a..a8ab0cec9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/en.json b/public/locales/en.json index 4b9f4ce6b..6c36a3940 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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> for more information.", "help": { "contacts": { "title": "Contacts", diff --git a/src/components/common/document-title/document-title.tsx b/src/components/common/document-title/document-title.tsx index 3154dc5f4..16399227f 100644 --- a/src/components/common/document-title/document-title.tsx +++ b/src/components/common/document-title/document-title.tsx @@ -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 = ({ 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 } diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index 5cb1ba533..9b63189bf 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -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() + const firstHeading = useRef() + + 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 ( +
setMarkdownContent(content)} content={markdownContent}/>} showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)} - right={} + right={} containerClassName={'overflow-hidden'}/>
) } - -export { Editor } diff --git a/src/components/editor/markdown-renderer/markdown-renderer.tsx b/src/components/editor/markdown-renderer/markdown-renderer.tsx index 57f5a02c0..797e66b4a 100644 --- a/src/components/editor/markdown-renderer/markdown-renderer.tsx +++ b/src/components/editor/markdown-renderer/markdown-renderer.tsx @@ -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 = ({ content, className, onTocChange, wide }) => { +export const MarkdownRenderer: React.FC = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => { const [tocAst, setTocAst] = useState() const [lastTocAst, setLastTocAst] = useState() + const [yamlError, setYamlError] = useState(false) + const rawMetaRef = useRef() + const oldMetaRef = useRef() + const firstHeadingRef = useRef() + const oldFirstHeadingRef = useRef() + + 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 = ({ 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 = ({ 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 = ({ content, cla }) return md - }, []) + }, [onMetaDataChange, onFirstHeadingChange]) useEffect(() => { if (onTocChange && tocAst && !equal(tocAst, lastTocAst)) { @@ -155,6 +224,10 @@ export const MarkdownRenderer: React.FC = ({ 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 = ({ content, cla return tryToReplaceNode(node, index, allReplacers, subNodeConverter) || convertNodeToElement(node, index, transform) } return ReactHtmlParser(html, { transform: transform }) - }, [content, markdownIt]) + }, [content, markdownIt, onMetaDataChange]) return (
+ + + + + + + {result} diff --git a/src/components/editor/renderer-window/markdown-render-window.tsx b/src/components/editor/renderer-window/markdown-render-window.tsx index 44caeea87..27b65948e 100644 --- a/src/components/editor/renderer-window/markdown-render-window.tsx +++ b/src/components/editor/renderer-window/markdown-render-window.tsx @@ -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 = ({ content, wide }) => { +export const MarkdownRenderWindow: React.FC = ({ content, onMetadataChange, onFirstHeadingChange, wide }) => { const [tocAst, setTocAst] = useState() const renderer = useRef(null) const { width } = useResizeObserver({ ref: renderer }) @@ -26,7 +29,10 @@ export const MarkdownRenderWindow: React.FC = ({ content, wid className={'flex-fill'} content={content} wide={wide} - onTocChange={(tocAst) => setTocAst(tocAst)}/> + onTocChange={(tocAst) => setTocAst(tocAst)} + onMetaDataChange={onMetadataChange} + onFirstHeadingChange={onFirstHeadingChange} + />
= 1280 && !!tocAst}> diff --git a/src/components/editor/yaml-metadata/yaml-metadata.test.ts b/src/components/editor/yaml-metadata/yaml-metadata.test.ts new file mode 100644 index 000000000..7f9e09c24 --- /dev/null +++ b/src/components/editor/yaml-metadata/yaml-metadata.test.ts @@ -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, expectedFinished: Partial) => { + 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() + }) + }) + + it('opengraph title only', () => { + testMetadata(`--- + opengraph: + title: Testtitle + ___ + `, + { + opengraph: { + title: 'Testtitle' + } + }, + { + opengraph: new Map(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(Object.entries({ + title: 'Testtitle', + image: 'https://dummyimage.com/48.png', + 'image:type': 'image/png' + })) + }) + }) +}) diff --git a/src/components/editor/yaml-metadata/yaml-metadata.ts b/src/components/editor/yaml-metadata/yaml-metadata.ts new file mode 100644 index 000000000..6e0ffd295 --- /dev/null +++ b/src/components/editor/yaml-metadata/yaml-metadata.ts @@ -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 + + 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(Object.entries(rawData.opengraph)) : new Map() + } +} diff --git a/src/external-types/markdown-it-front-matter/index.d.ts b/src/external-types/markdown-it-front-matter/index.d.ts new file mode 100644 index 000000000..ab9c8f145 --- /dev/null +++ b/src/external-types/markdown-it-front-matter/index.d.ts @@ -0,0 +1,5 @@ +declare module 'markdown-it-front-matter' { + import MarkdownIt from 'markdown-it/lib' + const markdownItFrontMatter: MarkdownIt.PluginSimple + export = markdownItFrontMatter +} diff --git a/yarn.lock b/yarn.lock index ef31e19d4..ac23b3136 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"