Add slide mode with reveal.js

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-10-04 12:50:39 +02:00
parent 29565f8f89
commit 36e445e631
70 changed files with 1225 additions and 323 deletions

View file

@ -8,87 +8,87 @@ import { extractFrontmatter } from './extract-frontmatter'
import { PresentFrontmatterExtractionResult } from './types'
describe('frontmatter extraction', () => {
describe('frontmatterPresent property', () => {
describe('isPresent property', () => {
it('is false when note does not contain three dashes at all', () => {
const testNote = 'abcdef\nmore text'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note does not start with three dashes', () => {
const testNote = '\n---\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note start with less than three dashes', () => {
const testNote = '--\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note starts with three dashes but contains other characters in the same line', () => {
const testNote = '--- a\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note has no ending marker for frontmatter', () => {
const testNote = '---\nthis is not frontmatter\nbecause\nthere is no\nend marker'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note end marker is present but with not the same amount of dashes as start marker', () => {
const testNote = '---\nthis is not frontmatter\n----\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is true when note end marker is present with the same amount of dashes as start marker', () => {
const testNote = '---\nthis is frontmatter\n---\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
it('is true when note end marker is present with the same amount of dashes as start marker but without content', () => {
const testNote = '---\nthis is frontmatter\n---'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
it('is true when note end marker is present with the same amount of dots as start marker', () => {
const testNote = '---\nthis is frontmatter\n...\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
})
describe('frontmatterLines property', () => {
describe('lineOffset property', () => {
it('is correct for single line frontmatter without content', () => {
const testNote = '---\nsingle line frontmatter\n...'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(3)
expect(extraction.lineOffset).toEqual(3)
})
it('is correct for single line frontmatter with content', () => {
const testNote = '---\nsingle line frontmatter\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(3)
expect(extraction.lineOffset).toEqual(3)
})
it('is correct for multi-line frontmatter without content', () => {
const testNote = '---\nabc\n123\ndef\n...'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(5)
expect(extraction.lineOffset).toEqual(5)
})
it('is correct for multi-line frontmatter with content', () => {
const testNote = '---\nabc\n123\ndef\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(5)
expect(extraction.lineOffset).toEqual(5)
})
})
describe('rawFrontmatterText property', () => {
describe('rawText property', () => {
it('contains single-line frontmatter text', () => {
const testNote = '---\nsingle-line\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.rawFrontmatterText).toEqual('single-line')
expect(extraction.rawText).toEqual('single-line')
})
it('contains multi-line frontmatter text', () => {
const testNote = '---\nmulti\nline\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.rawFrontmatterText).toEqual('multi\nline')
expect(extraction.rawText).toEqual('multi\nline')
})
})
})

View file

@ -13,7 +13,7 @@ const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/
* A valid frontmatter block requires the content to start with a line containing at least three dashes.
* The block is terminated by a line containing the same amount of dashes or dots as the first line.
* @param content The multiline string from which the frontmatter should be extracted.
* @return { frontmatterPresent } false if no frontmatter block could be found, true if a block was found.
* @return { isPresent } false if no frontmatter block could be found, true if a block was found.
* { rawFrontmatterText } if a block was found, this property contains the extracted text without the fencing.
* { frontmatterLines } if a block was found, this property contains the number of lines to skip from the
* given multiline string for retrieving the non-frontmatter content.
@ -22,19 +22,19 @@ export const extractFrontmatter = (content: string): FrontmatterExtractionResult
const lines = content.split('\n')
if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) {
return {
frontmatterPresent: false
isPresent: false
}
}
for (let i = 1; i < lines.length; i++) {
if (lines[i].length === lines[0].length && FRONTMATTER_END_REGEX.test(lines[i])) {
return {
frontmatterPresent: true,
rawFrontmatterText: lines.slice(1, i).join('\n'),
frontmatterLines: i + 1
isPresent: true,
rawText: lines.slice(1, i).join('\n'),
lineOffset: i + 1
}
}
}
return {
frontmatterPresent: false
isPresent: false
}
}

View file

@ -4,27 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from './note-frontmatter'
import { createNoteFrontmatterFromYaml } from './note-frontmatter'
describe('yaml frontmatter', () => {
it('should parse "title"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('title: test')
const noteFrontmatter = createNoteFrontmatterFromYaml('title: test')
expect(noteFrontmatter.title).toEqual('test')
})
it('should parse "robots"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('robots: index, follow')
const noteFrontmatter = createNoteFrontmatterFromYaml('robots: index, follow')
expect(noteFrontmatter.robots).toEqual('index, follow')
})
it('should parse the deprecated tags syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('tags: test123, abc')
const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc')
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
})
it('should parse the tags list syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`tags:
const noteFrontmatter = createNoteFrontmatterFromYaml(`tags:
- test123
- abc
`)
@ -33,30 +33,30 @@ describe('yaml frontmatter', () => {
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml("tags: ['test123', 'abc']")
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse "breaks"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('breaks: false')
const noteFrontmatter = createNoteFrontmatterFromYaml('breaks: false')
expect(noteFrontmatter.breaks).toEqual(false)
})
it('should parse an empty opengraph object', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('opengraph:')
const noteFrontmatter = createNoteFrontmatterFromYaml('opengraph:')
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
})
it('should parse an opengraph title', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
`)
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
})
it('should parse multiple opengraph values', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png

View file

@ -6,12 +6,13 @@
// import { RevealOptions } from 'reveal.js'
import { load } from 'js-yaml'
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter } from './types'
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter, SlideOptions } from './types'
import { initialSlideOptions } from '../../../redux/note-details/initial-state'
/**
* Class that represents the parsed frontmatter metadata of a note.
*/
export class NoteFrontmatter {
export interface NoteFrontmatter {
title: string
description: string
tags: string[]
@ -24,47 +25,98 @@ export class NoteFrontmatter {
disqus: string
type: NoteType
opengraph: Map<string, string>
slideOptions: SlideOptions
}
/**
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
*/
constructor(rawData: RawNoteFrontmatter) {
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.lang = (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en'
this.type =
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT
this.dir =
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
NoteTextDirection.LTR
if (typeof rawData?.tags === 'string') {
this.tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
this.deprecatedTagsSyntax = true
} else if (typeof rawData?.tags === 'object') {
this.tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
this.deprecatedTagsSyntax = false
} else {
this.tags = []
this.deprecatedTagsSyntax = false
}
this.opengraph = rawData?.opengraph
? new Map<string, string>(Object.entries(rawData.opengraph))
: new Map<string, string>()
/**
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
*/
export const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
let tags: string[]
let deprecatedTagsSyntax: boolean
if (typeof rawData?.tags === 'string') {
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
deprecatedTagsSyntax = true
} else if (typeof rawData?.tags === 'object') {
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
deprecatedTagsSyntax = false
} else {
tags = []
deprecatedTagsSyntax = false
}
/**
* Creates a new frontmatter metadata instance based on a raw yaml string.
* @param rawYaml The frontmatter content in yaml format.
* @throws Error when the content string is invalid yaml.
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
*/
static createFromYaml(rawYaml: string): NoteFrontmatter {
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
return new NoteFrontmatter(rawNoteFrontmatter)
return {
title: rawData.title ?? '',
description: rawData.description ?? '',
robots: rawData.robots ?? '',
breaks: rawData.breaks ?? true,
GA: rawData.GA ?? '',
disqus: rawData.disqus ?? '',
lang: (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en',
type:
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT,
dir:
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
NoteTextDirection.LTR,
opengraph: rawData?.opengraph
? new Map<string, string>(Object.entries(rawData.opengraph))
: new Map<string, string>(),
slideOptions: parseSlideOptions(rawData),
tags,
deprecatedTagsSyntax
}
}
/**
* Parses the {@link SlideOptions} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed slide options
*/
const parseSlideOptions = (rawData: RawNoteFrontmatter): SlideOptions => {
const rawSlideOptions = rawData?.slideOptions
return {
autoSlide: parseNumber(rawSlideOptions?.autoSlide) ?? initialSlideOptions.autoSlide,
transition: rawSlideOptions?.transition ?? initialSlideOptions.transition,
backgroundTransition: rawSlideOptions?.backgroundTransition ?? initialSlideOptions.backgroundTransition,
autoSlideStoppable: parseBoolean(rawSlideOptions?.autoSlideStoppable) ?? initialSlideOptions.autoSlideStoppable,
slideNumber: parseBoolean(rawSlideOptions?.slideNumber) ?? initialSlideOptions.slideNumber
}
}
/**
* Parses an unknown variable into a boolean.
*
* @param rawData The raw data
* @return The parsed boolean or undefined if it's not possible to parse the data.
*/
const parseBoolean = (rawData: unknown | undefined): boolean | undefined => {
return rawData === undefined ? undefined : rawData === true
}
/**
* Parses an unknown variable into a number.
*
* @param rawData The raw data
* @return The parsed number or undefined if it's not possible to parse the data.
*/
const parseNumber = (rawData: unknown | undefined): number | undefined => {
if (rawData === undefined) {
return undefined
}
const numValue = Number(rawData)
return isNaN(numValue) ? undefined : numValue
}
/**
* Creates a new frontmatter metadata instance based on a raw yaml string.
* @param rawYaml The frontmatter content in yaml format.
* @throws Error when the content string is invalid yaml.
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
*/
export const createNoteFrontmatterFromYaml = (rawYaml: string): NoteFrontmatter => {
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
return parseRawNoteFrontmatter(rawNoteFrontmatter)
}

View file

@ -4,22 +4,33 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RevealOptions } from 'reveal.js'
export type FrontmatterExtractionResult = PresentFrontmatterExtractionResult | NonPresentFrontmatterExtractionResult
export type WantedRevealOptions =
| 'autoSlide'
| 'autoSlideStoppable'
| 'transition'
| 'backgroundTransition'
| 'slideNumber'
export type SlideOptions = Required<Pick<RevealOptions, WantedRevealOptions>>
export interface RendererFrontmatterInfo {
offsetLines: number
lineOffset: number
frontmatterInvalid: boolean
deprecatedSyntax: boolean
slideOptions: SlideOptions
}
export interface PresentFrontmatterExtractionResult {
frontmatterPresent: true
rawFrontmatterText: string
frontmatterLines: number
isPresent: true
rawText: string
lineOffset: number
}
interface NonPresentFrontmatterExtractionResult {
frontmatterPresent: false
isPresent: false
}
export interface RawNoteFrontmatter {
@ -33,7 +44,7 @@ export interface RawNoteFrontmatter {
GA: string | undefined
disqus: string | undefined
type: string | undefined
slideOptions: unknown
slideOptions: { [key: string]: string } | null
opengraph: { [key: string]: string } | null
}