mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04:00
Move frontmatter extraction from renderer to redux (#1413)
This commit is contained in:
parent
7fb7c55877
commit
04e16d8880
34 changed files with 680 additions and 589 deletions
|
@ -63,7 +63,6 @@
|
||||||
"markdown-it-deflist": "2.1.0",
|
"markdown-it-deflist": "2.1.0",
|
||||||
"markdown-it-emoji": "2.0.0",
|
"markdown-it-emoji": "2.0.0",
|
||||||
"markdown-it-footnote": "3.0.3",
|
"markdown-it-footnote": "3.0.3",
|
||||||
"markdown-it-front-matter": "0.2.3",
|
|
||||||
"markdown-it-imsize": "2.0.1",
|
"markdown-it-imsize": "2.0.1",
|
||||||
"markdown-it-ins": "3.0.1",
|
"markdown-it-ins": "3.0.1",
|
||||||
"markdown-it-mark": "3.0.1",
|
"markdown-it-mark": "3.0.1",
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NoteDto } from './types'
|
|
||||||
import { NoteDetails } from '../../redux/note-details/types'
|
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import { initialState } from '../../redux/note-details/reducers'
|
|
||||||
|
|
||||||
export const noteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
|
||||||
return {
|
|
||||||
markdownContent: note.content,
|
|
||||||
frontmatter: initialState.frontmatter,
|
|
||||||
id: note.metadata.id,
|
|
||||||
noteTitle: initialState.noteTitle,
|
|
||||||
createTime: DateTime.fromISO(note.metadata.createTime),
|
|
||||||
lastChange: {
|
|
||||||
userName: note.metadata.updateUser.userName,
|
|
||||||
timestamp: DateTime.fromISO(note.metadata.updateTime)
|
|
||||||
},
|
|
||||||
firstHeading: initialState.firstHeading,
|
|
||||||
viewCount: note.metadata.viewCount,
|
|
||||||
alias: note.metadata.alias,
|
|
||||||
authorship: note.metadata.editedBy
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extractFrontmatter } from './extract-frontmatter'
|
||||||
|
import { PresentFrontmatterExtractionResult } from './types'
|
||||||
|
|
||||||
|
describe('frontmatter extraction', () => {
|
||||||
|
describe('frontmatterPresent 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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('frontmatterLines 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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rawFrontmatterText 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')
|
||||||
|
})
|
||||||
|
it('contains multi-line frontmatter text', () => {
|
||||||
|
const testNote = '---\nmulti\nline\n...\ncontent'
|
||||||
|
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||||
|
expect(extraction.rawFrontmatterText).toEqual('multi\nline')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { FrontmatterExtractionResult } from './types'
|
||||||
|
|
||||||
|
const FRONTMATTER_BEGIN_REGEX = /^-{3,}$/
|
||||||
|
const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a frontmatter block from a given multiline string.
|
||||||
|
* 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.
|
||||||
|
* { 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.
|
||||||
|
*/
|
||||||
|
export const extractFrontmatter = (content: string): FrontmatterExtractionResult => {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) {
|
||||||
|
return {
|
||||||
|
frontmatterPresent: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
frontmatterPresent: false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NoteFrontmatter } from './note-frontmatter'
|
||||||
|
|
||||||
|
describe('yaml frontmatter', () => {
|
||||||
|
it('should parse "title"', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml('title: test')
|
||||||
|
expect(noteFrontmatter.title).toEqual('test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse "robots"', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml('robots: index, follow')
|
||||||
|
expect(noteFrontmatter.robots).toEqual('index, follow')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse the deprecated tags syntax', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml('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:
|
||||||
|
- test123
|
||||||
|
- abc
|
||||||
|
`)
|
||||||
|
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||||
|
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse the tag inline-list syntax', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml("tags: ['test123', 'abc']")
|
||||||
|
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||||
|
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse "breaks"', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml('breaks: false')
|
||||||
|
expect(noteFrontmatter.breaks).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse an empty opengraph object', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml('opengraph:')
|
||||||
|
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse an opengraph title', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
|
||||||
|
title: Testtitle
|
||||||
|
`)
|
||||||
|
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse multiple opengraph values', () => {
|
||||||
|
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
|
||||||
|
title: Testtitle
|
||||||
|
image: https://dummyimage.com/48.png
|
||||||
|
image:type: image/png
|
||||||
|
`)
|
||||||
|
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
|
||||||
|
expect(noteFrontmatter.opengraph.get('image')).toEqual('https://dummyimage.com/48.png')
|
||||||
|
expect(noteFrontmatter.opengraph.get('image:type')).toEqual('image/png')
|
||||||
|
})
|
||||||
|
})
|
70
src/components/common/note-frontmatter/note-frontmatter.ts
Normal file
70
src/components/common/note-frontmatter/note-frontmatter.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import { RevealOptions } from 'reveal.js'
|
||||||
|
import { load } from 'js-yaml'
|
||||||
|
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that represents the parsed frontmatter metadata of a note.
|
||||||
|
*/
|
||||||
|
export class NoteFrontmatter {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
tags: string[]
|
||||||
|
deprecatedTagsSyntax: boolean
|
||||||
|
robots: string
|
||||||
|
lang: typeof ISO6391[number]
|
||||||
|
dir: NoteTextDirection
|
||||||
|
breaks: boolean
|
||||||
|
GA: string
|
||||||
|
disqus: string
|
||||||
|
type: NoteType
|
||||||
|
opengraph: 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.
|
||||||
|
*/
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,23 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// import { RevealOptions } from 'reveal.js'
|
export type FrontmatterExtractionResult = PresentFrontmatterExtractionResult | NonPresentFrontmatterExtractionResult
|
||||||
|
|
||||||
|
export interface RendererFrontmatterInfo {
|
||||||
|
offsetLines: number
|
||||||
|
frontmatterInvalid: boolean
|
||||||
|
deprecatedSyntax: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresentFrontmatterExtractionResult {
|
||||||
|
frontmatterPresent: true
|
||||||
|
rawFrontmatterText: string
|
||||||
|
frontmatterLines: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NonPresentFrontmatterExtractionResult {
|
||||||
|
frontmatterPresent: false
|
||||||
|
}
|
||||||
|
|
||||||
export interface RawNoteFrontmatter {
|
export interface RawNoteFrontmatter {
|
||||||
title: string | undefined
|
title: string | undefined
|
||||||
|
@ -235,52 +251,3 @@ export enum NoteTextDirection {
|
||||||
LTR = 'ltr',
|
LTR = 'ltr',
|
||||||
RTL = 'rtl'
|
RTL = 'rtl'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NoteFrontmatter {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
tags: string[]
|
|
||||||
deprecatedTagsSyntax: boolean
|
|
||||||
robots: string
|
|
||||||
lang: typeof ISO6391[number]
|
|
||||||
dir: NoteTextDirection
|
|
||||||
breaks: boolean
|
|
||||||
GA: string
|
|
||||||
disqus: string
|
|
||||||
type: NoteType
|
|
||||||
// slideOptions: RevealOptions
|
|
||||||
opengraph: Map<string, string>
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
|
|
||||||
transition: 'none',
|
|
||||||
theme: 'white'
|
|
||||||
} */
|
|
||||||
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>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ import { useParams } from 'react-router'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
||||||
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
||||||
import { setNoteFrontmatter, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { AppBar, AppBarMode } from '../editor-page/app-bar/app-bar'
|
import { AppBar, AppBarMode } from '../editor-page/app-bar/app-bar'
|
||||||
|
@ -32,7 +32,6 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
useDocumentTitleWithNoteTitle()
|
useDocumentTitleWithNoteTitle()
|
||||||
|
|
||||||
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
|
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
|
||||||
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
|
|
||||||
const [error, loading] = useLoadNoteFromServer()
|
const [error, loading] = useLoadNoteFromServer()
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
const noteDetails = useApplicationState((state) => state.noteDetails)
|
const noteDetails = useApplicationState((state) => state.noteDetails)
|
||||||
|
@ -60,7 +59,6 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
frameClasses={'flex-fill h-100 w-100'}
|
frameClasses={'flex-fill h-100 w-100'}
|
||||||
markdownContent={markdownContent}
|
markdownContent={markdownContent}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onFrontmatterChange={onFrontmatterChange}
|
|
||||||
rendererType={RendererType.DOCUMENT}
|
rendererType={RendererType.DOCUMENT}
|
||||||
/>
|
/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { EditorViewMode } from './editor-view-mode'
|
||||||
import { HelpButton } from './help-button/help-button'
|
import { HelpButton } from './help-button/help-button'
|
||||||
import { NavbarBranding } from './navbar-branding'
|
import { NavbarBranding } from './navbar-branding'
|
||||||
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
|
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
|
||||||
import { NoteType } from '../note-frontmatter/note-frontmatter'
|
import { NoteType } from '../../common/note-frontmatter/types'
|
||||||
import { SlideModeButton } from './slide-mode-button'
|
import { SlideModeButton } from './slide-mode-button'
|
||||||
import { ReadOnlyModeButton } from './read-only-mode-button'
|
import { ReadOnlyModeButton } from './read-only-mode-button'
|
||||||
import { NewNoteButton } from './new-note-button'
|
import { NewNoteButton } from './new-note-button'
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { CopyableField } from '../../../common/copyable/copyable-field/copyable-
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import { EditorPagePathParams } from '../../editor-page'
|
import { EditorPagePathParams } from '../../editor-page'
|
||||||
import { NoteType } from '../../note-frontmatter/note-frontmatter'
|
import { NoteType } from '../../../common/note-frontmatter/types'
|
||||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
|
||||||
export interface ShareModalProps {
|
export interface ShareModalProps {
|
||||||
|
|
|
@ -11,8 +11,7 @@ import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-t
|
||||||
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
||||||
import {
|
import {
|
||||||
setCheckboxInMarkdownContent,
|
setCheckboxInMarkdownContent,
|
||||||
setNoteFrontmatter,
|
setNoteContent,
|
||||||
setNoteMarkdownContent,
|
|
||||||
updateNoteTitleByFirstHeading
|
updateNoteTitleByFirstHeading
|
||||||
} from '../../redux/note-details/methods'
|
} from '../../redux/note-details/methods'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
|
@ -50,6 +49,7 @@ export const EditorPage: React.FC = () => {
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
|
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
|
||||||
|
|
||||||
|
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
|
||||||
const editorMode: EditorMode = useApplicationState((state) => state.editorConfig.editorMode)
|
const editorMode: EditorMode = useApplicationState((state) => state.editorConfig.editorMode)
|
||||||
const editorSyncScroll: boolean = useApplicationState((state) => state.editorConfig.syncScroll)
|
const editorSyncScroll: boolean = useApplicationState((state) => state.editorConfig.syncScroll)
|
||||||
|
|
||||||
|
@ -98,14 +98,14 @@ export const EditorPage: React.FC = () => {
|
||||||
const leftPane = useMemo(
|
const leftPane = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<EditorPane
|
<EditorPane
|
||||||
onContentChange={setNoteMarkdownContent}
|
onContentChange={setNoteContent}
|
||||||
content={markdownContent}
|
content={documentContent}
|
||||||
scrollState={scrollState.editorScrollState}
|
scrollState={scrollState.editorScrollState}
|
||||||
onScroll={onEditorScroll}
|
onScroll={onEditorScroll}
|
||||||
onMakeScrollSource={setEditorToScrollSource}
|
onMakeScrollSource={setEditorToScrollSource}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
|
[documentContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
|
||||||
)
|
)
|
||||||
|
|
||||||
const rightPane = useMemo(
|
const rightPane = useMemo(
|
||||||
|
@ -116,7 +116,6 @@ export const EditorPage: React.FC = () => {
|
||||||
onMakeScrollSource={setRendererToScrollSource}
|
onMakeScrollSource={setRendererToScrollSource}
|
||||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||||
onTaskCheckedChange={setCheckboxInMarkdownContent}
|
onTaskCheckedChange={setCheckboxInMarkdownContent}
|
||||||
onFrontmatterChange={setNoteFrontmatter}
|
|
||||||
onScroll={onMarkdownRendererScroll}
|
onScroll={onMarkdownRendererScroll}
|
||||||
scrollState={scrollState.rendererScrollState}
|
scrollState={scrollState.rendererScrollState}
|
||||||
rendererType={RendererType.DOCUMENT}
|
rendererType={RendererType.DOCUMENT}
|
||||||
|
|
|
@ -1,189 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import yaml from 'js-yaml'
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import frontmatter from 'markdown-it-front-matter'
|
|
||||||
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
|
|
||||||
|
|
||||||
describe('yaml frontmatter', () => {
|
|
||||||
const testFrontmatter = (input: string): NoteFrontmatter => {
|
|
||||||
let processedFrontmatter: NoteFrontmatter | undefined = undefined
|
|
||||||
const md = new MarkdownIt('default', {
|
|
||||||
html: true,
|
|
||||||
breaks: true,
|
|
||||||
langPrefix: '',
|
|
||||||
typographer: true
|
|
||||||
})
|
|
||||||
md.use(frontmatter, (rawMeta: string) => {
|
|
||||||
const parsedFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter | undefined
|
|
||||||
expect(parsedFrontmatter).not.toBe(undefined)
|
|
||||||
if (parsedFrontmatter === undefined) {
|
|
||||||
fail('Parsed frontmatter is undefined')
|
|
||||||
}
|
|
||||||
processedFrontmatter = new NoteFrontmatter(parsedFrontmatter)
|
|
||||||
})
|
|
||||||
|
|
||||||
md.render(input)
|
|
||||||
|
|
||||||
if (processedFrontmatter === undefined) {
|
|
||||||
fail('NoteFrontmatter is undefined')
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedFrontmatter
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should parse "title"', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
title: test
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.title).toEqual('test')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse "robots"', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
robots: index, follow
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.robots).toEqual('index, follow')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse the deprecated tags syntax', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
tags: test123, abc
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse the tags list syntax', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
tags:
|
|
||||||
- test123
|
|
||||||
- abc
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse the tag inline-list syntax', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
tags: ['test123', 'abc']
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse "breaks"', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
breaks: false
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.breaks).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
it('slideOptions nothing', () => {
|
|
||||||
testFrontmatter(`---
|
|
||||||
slideOptions:
|
|
||||||
___
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
slideOptions: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slideOptions: {
|
|
||||||
theme: 'white',
|
|
||||||
transition: 'none'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('slideOptions.theme only', () => {
|
|
||||||
testFrontmatter(`---
|
|
||||||
slideOptions:
|
|
||||||
theme: sky
|
|
||||||
___
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
slideOptions: {
|
|
||||||
theme: 'sky',
|
|
||||||
transition: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slideOptions: {
|
|
||||||
theme: 'sky',
|
|
||||||
transition: 'none'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('slideOptions full', () => {
|
|
||||||
testFrontmatter(`---
|
|
||||||
slideOptions:
|
|
||||||
transition: zoom
|
|
||||||
theme: sky
|
|
||||||
___
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
slideOptions: {
|
|
||||||
theme: 'sky',
|
|
||||||
transition: 'zoom'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slideOptions: {
|
|
||||||
theme: 'sky',
|
|
||||||
transition: 'zoom'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
it('should parse an empty opengraph object', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
opengraph:
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse an opengraph title', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
opengraph:
|
|
||||||
title: Testtitle
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should opengraph values', () => {
|
|
||||||
const noteFrontmatter = testFrontmatter(`---
|
|
||||||
opengraph:
|
|
||||||
title: Testtitle
|
|
||||||
image: https://dummyimage.com/48.png
|
|
||||||
image:type: image/png
|
|
||||||
___
|
|
||||||
`)
|
|
||||||
|
|
||||||
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
|
|
||||||
expect(noteFrontmatter.opengraph.get('image')).toEqual('https://dummyimage.com/48.png')
|
|
||||||
expect(noteFrontmatter.opengraph.get('image:type')).toEqual('image/png')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -25,7 +25,6 @@ export interface RenderIframeProps extends RendererProps {
|
||||||
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
markdownContent,
|
markdownContent,
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
onFrontmatterChange,
|
|
||||||
scrollState,
|
scrollState,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
@ -39,6 +38,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
||||||
|
|
||||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||||
|
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
|
||||||
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
|
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
|
||||||
const renderPageUrl = `${rendererOrigin}render`
|
const renderPageUrl = `${rendererOrigin}render`
|
||||||
const resetRendererReady = useCallback(() => setRendererStatus(false), [])
|
const resetRendererReady = useCallback(() => setRendererStatus(false), [])
|
||||||
|
@ -67,11 +67,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
return () => iframeCommunicator.onFirstHeadingChange(undefined)
|
return () => iframeCommunicator.onFirstHeadingChange(undefined)
|
||||||
}, [iframeCommunicator, onFirstHeadingChange])
|
}, [iframeCommunicator, onFirstHeadingChange])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
iframeCommunicator.onFrontmatterChange(onFrontmatterChange)
|
|
||||||
return () => iframeCommunicator.onFrontmatterChange(undefined)
|
|
||||||
}, [iframeCommunicator, onFrontmatterChange])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
iframeCommunicator.onSetScrollState(onScroll)
|
iframeCommunicator.onSetScrollState(onScroll)
|
||||||
return () => iframeCommunicator.onSetScrollState(undefined)
|
return () => iframeCommunicator.onSetScrollState(undefined)
|
||||||
|
@ -128,6 +123,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||||
}
|
}
|
||||||
}, [iframeCommunicator, markdownContent, rendererReady])
|
}, [iframeCommunicator, markdownContent, rendererReady])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady && frontmatterInfo !== undefined) {
|
||||||
|
iframeCommunicator.sendSetFrontmatterInfo(frontmatterInfo)
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, rendererReady, frontmatterInfo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ShowOnPropChangeImageLightbox details={lightboxDetails} />
|
<ShowOnPropChangeImageLightbox details={lightboxDetails} />
|
||||||
|
|
|
@ -10,14 +10,13 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import links from '../../../links.json'
|
import links from '../../../links.json'
|
||||||
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
|
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { CommonModalProps } from '../../common/modals/common-modal'
|
||||||
|
|
||||||
export const YamlArrayDeprecationAlert: React.FC = () => {
|
export const YamlArrayDeprecationAlert: React.FC<Partial<CommonModalProps>> = ({ show }) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const yamlDeprecatedTags = useApplicationState((state) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={yamlDeprecatedTags}>
|
<ShowIf condition={!!show}>
|
||||||
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
|
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
|
||||||
<span className={'text-wrap'}>
|
<span className={'text-wrap'}>
|
||||||
<span className={'text-wrap'}>
|
<span className={'text-wrap'}>
|
||||||
|
|
|
@ -8,17 +8,17 @@ import React, { useCallback } from 'react'
|
||||||
import sanitize from 'sanitize-filename'
|
import sanitize from 'sanitize-filename'
|
||||||
import { store } from '../../../redux'
|
import { store } from '../../../redux'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
|
||||||
import { download } from '../../common/download/download'
|
import { download } from '../../common/download/download'
|
||||||
import { SidebarButton } from './sidebar-button'
|
import { SidebarButton } from './sidebar-button'
|
||||||
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
|
||||||
export const ExportMarkdownSidebarEntry: React.FC = () => {
|
export const ExportMarkdownSidebarEntry: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
const sanitized = sanitize(store.getState().noteDetails.noteTitle)
|
const sanitized = sanitize(store.getState().noteDetails.noteTitle)
|
||||||
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
|
download(documentContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
|
||||||
}, [markdownContent, t])
|
}, [documentContent, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarButton data-cy={'menu-export-markdown'} onClick={onClick} icon={'file-text'}>
|
<SidebarButton data-cy={'menu-export-markdown'} onClick={onClick} icon={'file-text'}>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import React, { Fragment, useCallback, useRef } from 'react'
|
import React, { Fragment, useCallback, useRef } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
||||||
import { setNoteMarkdownContent } from '../../../redux/note-details/methods'
|
import { setNoteContent } from '../../../redux/note-details/methods'
|
||||||
import { SidebarButton } from './sidebar-button'
|
import { SidebarButton } from './sidebar-button'
|
||||||
import { UploadInput } from './upload-input'
|
import { UploadInput } from './upload-input'
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
fileReader.addEventListener('load', () => {
|
fileReader.addEventListener('load', () => {
|
||||||
const newContent = fileReader.result as string
|
const newContent = fileReader.result as string
|
||||||
setNoteMarkdownContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
|
setNoteContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
|
||||||
})
|
})
|
||||||
fileReader.addEventListener('loadend', () => {
|
fileReader.addEventListener('loadend', () => {
|
||||||
resolve()
|
resolve()
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
import React, { Ref, useCallback, useMemo, useRef } from 'react'
|
||||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||||
import './markdown-renderer.scss'
|
import './markdown-renderer.scss'
|
||||||
|
@ -12,7 +12,6 @@ import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||||
import { useComponentReplacers } from './hooks/use-component-replacers'
|
import { useComponentReplacers } from './hooks/use-component-replacers'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||||
|
@ -20,7 +19,6 @@ import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import { useOnRefChange } from './hooks/use-on-ref-change'
|
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
|
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
|
||||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||||
import { InvalidYamlAlert } from './invalid-yaml-alert'
|
|
||||||
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||||
|
|
||||||
export interface BasicMarkdownRendererProps {
|
export interface BasicMarkdownRendererProps {
|
||||||
|
@ -29,79 +27,57 @@ export interface BasicMarkdownRendererProps {
|
||||||
onAfterRendering?: () => void
|
onAfterRendering?: () => void
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
onTocChange?: (ast?: TocAst) => void
|
onTocChange?: (ast?: TocAst) => void
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
onImageClick?: ImageClickHandler
|
onImageClick?: ImageClickHandler
|
||||||
outerContainerRef?: Ref<HTMLDivElement>
|
outerContainerRef?: Ref<HTMLDivElement>
|
||||||
useAlternativeBreaks?: boolean
|
useAlternativeBreaks?: boolean
|
||||||
|
frontmatterLineOffset?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||||
className,
|
className,
|
||||||
content,
|
content,
|
||||||
additionalReplacers,
|
additionalReplacers,
|
||||||
onBeforeRendering,
|
|
||||||
onAfterRendering,
|
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onLineMarkerPositionChanged,
|
onLineMarkerPositionChanged,
|
||||||
onFrontmatterChange,
|
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
onTocChange,
|
onTocChange,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
onImageClick,
|
onImageClick,
|
||||||
outerContainerRef,
|
outerContainerRef,
|
||||||
useAlternativeBreaks
|
useAlternativeBreaks,
|
||||||
|
frontmatterLineOffset
|
||||||
}) => {
|
}) => {
|
||||||
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
|
||||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||||
const hasNewYamlError = useRef(false)
|
const hasNewYamlError = useRef(false)
|
||||||
const tocAst = useRef<TocAst>()
|
const tocAst = useRef<TocAst>()
|
||||||
const [showYamlError, setShowYamlError] = useState(false)
|
|
||||||
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
|
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
|
||||||
|
|
||||||
const markdownIt = useMemo(
|
const markdownIt = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new BasicMarkdownItConfigurator({
|
new BasicMarkdownItConfigurator({
|
||||||
useFrontmatter: !!onFrontmatterChange,
|
|
||||||
onParseError: (errorState) => (hasNewYamlError.current = errorState),
|
onParseError: (errorState) => (hasNewYamlError.current = errorState),
|
||||||
onRawMetaChange: (rawMeta) => (rawMetaRef.current = rawMeta),
|
|
||||||
onToc: (toc) => (tocAst.current = toc),
|
onToc: (toc) => (tocAst.current = toc),
|
||||||
onLineMarkers:
|
onLineMarkers:
|
||||||
onLineMarkerPositionChanged === undefined
|
onLineMarkerPositionChanged === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
|
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
|
||||||
useAlternativeBreaks
|
useAlternativeBreaks,
|
||||||
|
offsetLines: frontmatterLineOffset
|
||||||
}).buildConfiguredMarkdownIt(),
|
}).buildConfiguredMarkdownIt(),
|
||||||
[onFrontmatterChange, onLineMarkerPositionChanged, useAlternativeBreaks]
|
[onLineMarkerPositionChanged, useAlternativeBreaks, frontmatterLineOffset]
|
||||||
)
|
)
|
||||||
|
|
||||||
const clearFrontmatter = useCallback(() => {
|
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, frontmatterLineOffset)
|
||||||
hasNewYamlError.current = false
|
|
||||||
rawMetaRef.current = undefined
|
|
||||||
onBeforeRendering?.()
|
|
||||||
}, [onBeforeRendering])
|
|
||||||
|
|
||||||
const checkYamlErrorState = useCallback(() => {
|
|
||||||
setShowYamlError(hasNewYamlError.current)
|
|
||||||
onAfterRendering?.()
|
|
||||||
}, [onAfterRendering])
|
|
||||||
|
|
||||||
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl)
|
|
||||||
const replacers = useCallback(
|
const replacers = useCallback(
|
||||||
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
|
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
|
||||||
[additionalReplacers, baseReplacers]
|
[additionalReplacers, baseReplacers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const markdownReactDom = useConvertMarkdownToReactDom(
|
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers)
|
||||||
trimmedContent,
|
|
||||||
markdownIt,
|
|
||||||
replacers,
|
|
||||||
clearFrontmatter,
|
|
||||||
checkYamlErrorState
|
|
||||||
)
|
|
||||||
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
useCalculateLineMarkerPosition(
|
useCalculateLineMarkerPosition(
|
||||||
|
@ -112,17 +88,9 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
|
||||||
)
|
)
|
||||||
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
|
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
|
||||||
useOnRefChange(tocAst, onTocChange)
|
useOnRefChange(tocAst, onTocChange)
|
||||||
useOnRefChange(rawMetaRef, (newValue) => {
|
|
||||||
if (!newValue) {
|
|
||||||
onFrontmatterChange?.(undefined)
|
|
||||||
} else {
|
|
||||||
onFrontmatterChange?.(new NoteFrontmatter(newValue))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={outerContainerRef} className={'position-relative'}>
|
<div ref={outerContainerRef} className={'position-relative'}>
|
||||||
<InvalidYamlAlert show={showYamlError} />
|
|
||||||
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
|
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
|
||||||
<div
|
<div
|
||||||
ref={markdownBodyRef}
|
ref={markdownBodyRef}
|
||||||
|
|
|
@ -32,13 +32,15 @@ import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||||
* @param onTaskCheckedChange A callback that gets executed if a task checkbox gets clicked
|
* @param onTaskCheckedChange A callback that gets executed if a task checkbox gets clicked
|
||||||
* @param onImageClick A callback that should be executed if an image gets clicked
|
* @param onImageClick A callback that should be executed if an image gets clicked
|
||||||
* @param baseUrl The base url for relative links
|
* @param baseUrl The base url for relative links
|
||||||
|
* @param frontmatterLinesToSkip The number of lines of the frontmatter part to add this as offset to line-numbers.
|
||||||
*
|
*
|
||||||
* @return the created list
|
* @return the created list
|
||||||
*/
|
*/
|
||||||
export const useComponentReplacers = (
|
export const useComponentReplacers = (
|
||||||
onTaskCheckedChange?: TaskCheckedChangeHandler,
|
onTaskCheckedChange?: TaskCheckedChangeHandler,
|
||||||
onImageClick?: ImageClickHandler,
|
onImageClick?: ImageClickHandler,
|
||||||
baseUrl?: string
|
baseUrl?: string,
|
||||||
|
frontmatterLinesToSkip?: number
|
||||||
): (() => ComponentReplacer[]) =>
|
): (() => ComponentReplacer[]) =>
|
||||||
useCallback(
|
useCallback(
|
||||||
() => [
|
() => [
|
||||||
|
@ -59,8 +61,8 @@ export const useComponentReplacers = (
|
||||||
new HighlightedCodeReplacer(),
|
new HighlightedCodeReplacer(),
|
||||||
new ColoredBlockquoteReplacer(),
|
new ColoredBlockquoteReplacer(),
|
||||||
new KatexReplacer(),
|
new KatexReplacer(),
|
||||||
new TaskListReplacer(onTaskCheckedChange),
|
new TaskListReplacer(onTaskCheckedChange, frontmatterLinesToSkip),
|
||||||
new LinkReplacer(baseUrl)
|
new LinkReplacer(baseUrl)
|
||||||
],
|
],
|
||||||
[onImageClick, onTaskCheckedChange, baseUrl]
|
[onImageClick, onTaskCheckedChange, baseUrl, frontmatterLinesToSkip]
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger
|
||||||
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
||||||
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
||||||
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
||||||
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
||||||
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
||||||
|
@ -36,15 +35,13 @@ import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
||||||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||||
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
||||||
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
||||||
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
|
|
||||||
|
|
||||||
export interface ConfiguratorDetails {
|
export interface ConfiguratorDetails {
|
||||||
useFrontmatter: boolean
|
|
||||||
onParseError: (error: boolean) => void
|
onParseError: (error: boolean) => void
|
||||||
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void
|
|
||||||
onToc: (toc: TocAst) => void
|
onToc: (toc: TocAst) => void
|
||||||
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||||
useAlternativeBreaks?: boolean
|
useAlternativeBreaks?: boolean
|
||||||
|
offsetLines?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||||
|
@ -105,17 +102,8 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||||
spoilerContainer
|
spoilerContainer
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.options.useFrontmatter) {
|
|
||||||
this.configurations.push(
|
|
||||||
frontmatterExtract({
|
|
||||||
onParseError: this.options.onParseError,
|
|
||||||
onRawMetaChange: this.options.onRawMetaChange
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.onLineMarkers) {
|
if (this.options.onLineMarkers) {
|
||||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers))
|
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.offsetLines ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)
|
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import yaml from 'js-yaml'
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import frontmatter from 'markdown-it-front-matter'
|
|
||||||
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
|
|
||||||
interface FrontmatterPluginOptions {
|
|
||||||
onParseError: (error: boolean) => void
|
|
||||||
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const frontmatterExtract: (options: FrontmatterPluginOptions) => MarkdownIt.PluginSimple =
|
|
||||||
(options) => (markdownIt) => {
|
|
||||||
frontmatter(markdownIt, (rawMeta: string) => {
|
|
||||||
try {
|
|
||||||
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
|
||||||
options.onParseError(false)
|
|
||||||
options.onRawMetaChange(meta)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
options.onParseError(true)
|
|
||||||
options.onRawMetaChange({} as RawNoteFrontmatter)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -18,12 +18,13 @@ export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void
|
||||||
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
||||||
* It also provides a list of line numbers for the top level dom elements.
|
* It also provides a list of line numbers for the top level dom elements.
|
||||||
*/
|
*/
|
||||||
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple =
|
export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: number) => MarkdownIt.PluginSimple =
|
||||||
(options) => (md: MarkdownIt) => {
|
(options, offsetLines = 0) =>
|
||||||
|
(md: MarkdownIt) => {
|
||||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||||
md.core.ruler.push('line_number_marker', (state) => {
|
md.core.ruler.push('line_number_marker', (state) => {
|
||||||
const lineMarkers: LineMarkers[] = []
|
const lineMarkers: LineMarkers[] = []
|
||||||
tagTokens(state.tokens, lineMarkers)
|
tagTokens(state.tokens, lineMarkers, offsetLines)
|
||||||
if (options) {
|
if (options) {
|
||||||
options(lineMarkers)
|
options(lineMarkers)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,7 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
|
||||||
tokens.splice(tokenPosition, 0, startToken)
|
tokens.splice(tokenPosition, 0, startToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
|
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], offsetLines: number) => {
|
||||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||||
const token = tokens[tokenPosition]
|
const token = tokens[tokenPosition]
|
||||||
if (token.hidden) {
|
if (token.hidden) {
|
||||||
|
@ -71,14 +72,14 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
|
||||||
const endLineNumber = token.map[1] + 1
|
const endLineNumber = token.map[1] + 1
|
||||||
|
|
||||||
if (token.level === 0) {
|
if (token.level === 0) {
|
||||||
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
|
lineMarkers.push({ startLine: startLineNumber + offsetLines, endLine: endLineNumber + offsetLines })
|
||||||
}
|
}
|
||||||
|
|
||||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||||
tokenPosition += 1
|
tokenPosition += 1
|
||||||
|
|
||||||
if (token.children) {
|
if (token.children) {
|
||||||
tagTokens(token.children, lineMarkers)
|
tagTokens(token.children, lineMarkers, offsetLines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,18 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
|
||||||
*/
|
*/
|
||||||
export class TaskListReplacer extends ComponentReplacer {
|
export class TaskListReplacer extends ComponentReplacer {
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
private readonly frontmatterLinesOffset
|
||||||
|
|
||||||
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler, frontmatterLinesOffset?: number) {
|
||||||
super()
|
super()
|
||||||
this.onTaskCheckedChange = onTaskCheckedChange
|
this.onTaskCheckedChange = onTaskCheckedChange
|
||||||
|
this.frontmatterLinesOffset = frontmatterLinesOffset ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
const lineNum = Number(event.currentTarget.dataset.line)
|
const lineNum = Number(event.currentTarget.dataset.line)
|
||||||
if (this.onTaskCheckedChange) {
|
if (this.onTaskCheckedChange) {
|
||||||
this.onTaskCheckedChange(lineNum, event.currentTarget.checked)
|
this.onTaskCheckedChange(lineNum + this.frontmatterLinesOffset, event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { IframeCommunicator } from './iframe-communicator'
|
import { IframeCommunicator } from './iframe-communicator'
|
||||||
import {
|
import {
|
||||||
|
@ -14,6 +12,7 @@ import {
|
||||||
RendererToEditorIframeMessage,
|
RendererToEditorIframeMessage,
|
||||||
RenderIframeMessageType
|
RenderIframeMessageType
|
||||||
} from './rendering-message'
|
} from './rendering-message'
|
||||||
|
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||||
|
|
||||||
export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
EditorToRendererIframeMessage,
|
EditorToRendererIframeMessage,
|
||||||
|
@ -22,7 +21,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
private onSetScrollSourceToRendererHandler?: () => void
|
private onSetScrollSourceToRendererHandler?: () => void
|
||||||
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
|
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
private onFirstHeadingChangeHandler?: (heading?: string) => void
|
private onFirstHeadingChangeHandler?: (heading?: string) => void
|
||||||
private onFrontmatterChangeHandler?: (frontmatter?: NoteFrontmatter) => void
|
|
||||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||||
private onRendererReadyHandler?: () => void
|
private onRendererReadyHandler?: () => void
|
||||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||||
|
@ -33,10 +31,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
this.onHeightChangeHandler = handler
|
this.onHeightChangeHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFrontmatterChange(handler?: (frontmatter?: NoteFrontmatter) => void): void {
|
|
||||||
this.onFrontmatterChangeHandler = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
public onImageClicked(handler?: (details: ImageDetails) => void): void {
|
public onImageClicked(handler?: (details: ImageDetails) => void): void {
|
||||||
this.onImageClickedHandler = handler
|
this.onImageClickedHandler = handler
|
||||||
}
|
}
|
||||||
|
@ -102,6 +96,13 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendSetFrontmatterInfo(frontmatterInfo: RendererFrontmatterInfo): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_FRONTMATTER_INFO,
|
||||||
|
frontmatterInfo: frontmatterInfo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
protected handleEvent(event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
protected handleEvent(event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||||
const renderMessage = event.data
|
const renderMessage = event.data
|
||||||
switch (renderMessage.type) {
|
switch (renderMessage.type) {
|
||||||
|
@ -121,9 +122,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
||||||
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
|
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
|
||||||
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
|
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
|
||||||
return false
|
return false
|
||||||
case RenderIframeMessageType.ON_SET_FRONTMATTER:
|
|
||||||
this.onFrontmatterChangeHandler?.(renderMessage.frontmatter)
|
|
||||||
return false
|
|
||||||
case RenderIframeMessageType.IMAGE_CLICKED:
|
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||||
this.onImageClickedHandler?.(renderMessage.details)
|
this.onImageClickedHandler?.(renderMessage.details)
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -8,18 +8,22 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { BaseConfiguration, RendererType } from './rendering-message'
|
import { BaseConfiguration, RendererType } from './rendering-message'
|
||||||
import { setDarkMode } from '../../redux/dark-mode/methods'
|
import { setDarkMode } from '../../redux/dark-mode/methods'
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { setNoteFrontmatter } from '../../redux/note-details/methods'
|
|
||||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||||
import { useImageClickHandler } from './hooks/use-image-click-handler'
|
import { useImageClickHandler } from './hooks/use-image-click-handler'
|
||||||
import { MarkdownDocument } from './markdown-document'
|
import { MarkdownDocument } from './markdown-document'
|
||||||
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
|
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
|
||||||
import { countWords } from './word-counter'
|
import { countWords } from './word-counter'
|
||||||
|
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||||
|
|
||||||
export const IframeMarkdownRenderer: React.FC = () => {
|
export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||||
|
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>({
|
||||||
|
offsetLines: 0,
|
||||||
|
frontmatterInvalid: false,
|
||||||
|
deprecatedSyntax: false
|
||||||
|
})
|
||||||
|
|
||||||
const iframeCommunicator = useIFrameRendererToEditorCommunicator()
|
const iframeCommunicator = useIFrameRendererToEditorCommunicator()
|
||||||
|
|
||||||
|
@ -37,6 +41,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||||
|
useEffect(() => iframeCommunicator.onSetFrontmatterInfo(setFrontmatterInfo), [iframeCommunicator, setFrontmatterInfo])
|
||||||
useEffect(
|
useEffect(
|
||||||
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
|
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
|
||||||
[iframeCommunicator, countWordsInRenderedDocument]
|
[iframeCommunicator, countWordsInRenderedDocument]
|
||||||
|
@ -60,14 +65,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
iframeCommunicator.sendSetScrollSourceToRenderer()
|
iframeCommunicator.sendSetScrollSourceToRenderer()
|
||||||
}, [iframeCommunicator])
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
const onFrontmatterChange = useCallback(
|
|
||||||
(frontmatter?: NoteFrontmatter) => {
|
|
||||||
setNoteFrontmatter(frontmatter)
|
|
||||||
iframeCommunicator.sendSetFrontmatter(frontmatter)
|
|
||||||
},
|
|
||||||
[iframeCommunicator]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onScroll = useCallback(
|
const onScroll = useCallback(
|
||||||
(scrollState: ScrollState) => {
|
(scrollState: ScrollState) => {
|
||||||
iframeCommunicator.sendSetScrollState(scrollState)
|
iframeCommunicator.sendSetScrollState(scrollState)
|
||||||
|
@ -97,11 +94,11 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onMakeScrollSource={onMakeScrollSource}
|
onMakeScrollSource={onMakeScrollSource}
|
||||||
onFrontmatterChange={onFrontmatterChange}
|
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
baseUrl={baseConfiguration.baseUrl}
|
baseUrl={baseConfiguration.baseUrl}
|
||||||
onImageClick={onImageClick}
|
onImageClick={onImageClick}
|
||||||
|
frontmatterInfo={frontmatterInfo}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case RendererType.INTRO:
|
case RendererType.INTRO:
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { IframeCommunicator } from './iframe-communicator'
|
import { IframeCommunicator } from './iframe-communicator'
|
||||||
import {
|
import {
|
||||||
|
@ -14,6 +13,7 @@ import {
|
||||||
RendererToEditorIframeMessage,
|
RendererToEditorIframeMessage,
|
||||||
RenderIframeMessageType
|
RenderIframeMessageType
|
||||||
} from './rendering-message'
|
} from './rendering-message'
|
||||||
|
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||||
|
|
||||||
export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
RendererToEditorIframeMessage,
|
RendererToEditorIframeMessage,
|
||||||
|
@ -24,6 +24,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||||
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
|
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
|
||||||
private onGetWordCountHandler?: () => void
|
private onGetWordCountHandler?: () => void
|
||||||
|
private onSetFrontmatterInfoHandler?: (frontmatterInfo: RendererFrontmatterInfo) => void
|
||||||
|
|
||||||
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
||||||
this.onSetBaseConfigurationHandler = handler
|
this.onSetBaseConfigurationHandler = handler
|
||||||
|
@ -45,6 +46,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
this.onGetWordCountHandler = handler
|
this.onGetWordCountHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onSetFrontmatterInfo(handler?: (frontmatterInfo: RendererFrontmatterInfo) => void): void {
|
||||||
|
this.onSetFrontmatterInfoHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
public sendRendererReady(): void {
|
public sendRendererReady(): void {
|
||||||
this.enableCommunication()
|
this.enableCommunication()
|
||||||
this.sendMessageToOtherSide({
|
this.sendMessageToOtherSide({
|
||||||
|
@ -73,13 +78,6 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendSetFrontmatter(frontmatter: NoteFrontmatter | undefined): void {
|
|
||||||
this.sendMessageToOtherSide({
|
|
||||||
type: RenderIframeMessageType.ON_SET_FRONTMATTER,
|
|
||||||
frontmatter: frontmatter
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendSetScrollState(scrollState: ScrollState): void {
|
public sendSetScrollState(scrollState: ScrollState): void {
|
||||||
this.sendMessageToOtherSide({
|
this.sendMessageToOtherSide({
|
||||||
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||||
|
@ -126,6 +124,9 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
||||||
case RenderIframeMessageType.GET_WORD_COUNT:
|
case RenderIframeMessageType.GET_WORD_COUNT:
|
||||||
this.onGetWordCountHandler?.()
|
this.onGetWordCountHandler?.()
|
||||||
return false
|
return false
|
||||||
|
case RenderIframeMessageType.SET_FRONTMATTER_INFO:
|
||||||
|
this.onSetFrontmatterInfoHandler?.(renderMessage.frontmatterInfo)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import useResizeObserver from 'use-resize-observer'
|
import useResizeObserver from 'use-resize-observer'
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
|
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
|
||||||
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
||||||
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
|
@ -17,10 +16,11 @@ import './markdown-document.scss'
|
||||||
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
|
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||||
|
import { InvalidYamlAlert } from '../markdown-renderer/invalid-yaml-alert'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||||
markdownContent: string
|
markdownContent: string
|
||||||
|
@ -33,13 +33,13 @@ export interface MarkdownDocumentProps extends RendererProps {
|
||||||
additionalRendererClasses?: string
|
additionalRendererClasses?: string
|
||||||
disableToc?: boolean
|
disableToc?: boolean
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
|
frontmatterInfo?: RendererFrontmatterInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
additionalOuterContainerClasses,
|
additionalOuterContainerClasses,
|
||||||
additionalRendererClasses,
|
additionalRendererClasses,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onFrontmatterChange,
|
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
@ -48,7 +48,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
onScroll,
|
onScroll,
|
||||||
scrollState,
|
scrollState,
|
||||||
onHeightChange,
|
onHeightChange,
|
||||||
disableToc
|
disableToc,
|
||||||
|
frontmatterInfo
|
||||||
}) => {
|
}) => {
|
||||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||||
const rendererSize = useResizeObserver({ ref: rendererRef.current })
|
const rendererSize = useResizeObserver({ ref: rendererRef.current })
|
||||||
|
@ -85,19 +86,20 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
onMouseEnter={onMakeScrollSource}>
|
onMouseEnter={onMakeScrollSource}>
|
||||||
<div className={'markdown-document-side'} />
|
<div className={'markdown-document-side'} />
|
||||||
<div className={'markdown-document-content'}>
|
<div className={'markdown-document-content'}>
|
||||||
<YamlArrayDeprecationAlert />
|
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
|
||||||
|
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
|
||||||
<BasicMarkdownRenderer
|
<BasicMarkdownRenderer
|
||||||
outerContainerRef={rendererRef}
|
outerContainerRef={rendererRef}
|
||||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||||
content={markdownContent}
|
content={markdownContent}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||||
onFrontmatterChange={onFrontmatterChange}
|
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
onTocChange={setTocAst}
|
onTocChange={setTocAst}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
onImageClick={onImageClick}
|
onImageClick={onImageClick}
|
||||||
useAlternativeBreaks={useAlternativeBreaks}
|
useAlternativeBreaks={useAlternativeBreaks}
|
||||||
|
frontmatterLineOffset={frontmatterInfo?.offsetLines}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'markdown-document-side pt-4'}>
|
<div className={'markdown-document-side pt-4'}>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
|
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||||
|
|
||||||
export enum RenderIframeMessageType {
|
export enum RenderIframeMessageType {
|
||||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
|
@ -14,12 +14,12 @@ export enum RenderIframeMessageType {
|
||||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||||
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
||||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
|
||||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
||||||
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
||||||
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED'
|
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
|
||||||
|
SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RendererToEditorSimpleMessage {
|
export interface RendererToEditorSimpleMessage {
|
||||||
|
@ -72,9 +72,9 @@ export interface OnFirstHeadingChangeMessage {
|
||||||
firstHeading: string | undefined
|
firstHeading: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnFrontmatterChangeMessage {
|
export interface SetFrontmatterInfoMessage {
|
||||||
type: RenderIframeMessageType.ON_SET_FRONTMATTER
|
type: RenderIframeMessageType.SET_FRONTMATTER_INFO
|
||||||
frontmatter: NoteFrontmatter | undefined
|
frontmatterInfo: RendererFrontmatterInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnHeightChangeMessage {
|
export interface OnHeightChangeMessage {
|
||||||
|
@ -93,12 +93,12 @@ export type EditorToRendererIframeMessage =
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| SetBaseUrlMessage
|
| SetBaseUrlMessage
|
||||||
| GetWordCountMessage
|
| GetWordCountMessage
|
||||||
|
| SetFrontmatterInfoMessage
|
||||||
|
|
||||||
export type RendererToEditorIframeMessage =
|
export type RendererToEditorIframeMessage =
|
||||||
| RendererToEditorSimpleMessage
|
| RendererToEditorSimpleMessage
|
||||||
| OnFirstHeadingChangeMessage
|
| OnFirstHeadingChangeMessage
|
||||||
| OnTaskCheckboxChangeMessage
|
| OnTaskCheckboxChangeMessage
|
||||||
| OnFrontmatterChangeMessage
|
|
||||||
| SetScrollStateMessage
|
| SetScrollStateMessage
|
||||||
| ImageClickedMessage
|
| ImageClickedMessage
|
||||||
| OnHeightChangeMessage
|
| OnHeightChangeMessage
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { DarkModeConfigReducer } from './dark-mode/reducers'
|
||||||
import { DarkModeConfig } from './dark-mode/types'
|
import { DarkModeConfig } from './dark-mode/types'
|
||||||
import { EditorConfigReducer } from './editor/reducers'
|
import { EditorConfigReducer } from './editor/reducers'
|
||||||
import { EditorConfig } from './editor/types'
|
import { EditorConfig } from './editor/types'
|
||||||
import { NoteDetailsReducer } from './note-details/reducers'
|
import { NoteDetailsReducer } from './note-details/reducer'
|
||||||
import { NoteDetails } from './note-details/types'
|
import { NoteDetails } from './note-details/types'
|
||||||
import { UserReducer } from './user/reducers'
|
import { UserReducer } from './user/reducers'
|
||||||
import { OptionalUserState } from './user/types'
|
import { OptionalUserState } from './user/types'
|
||||||
|
|
45
src/redux/note-details/initial-state.ts
Normal file
45
src/redux/note-details/initial-state.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NoteDetails } from './types'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import { NoteTextDirection, NoteType } from '../../components/common/note-frontmatter/types'
|
||||||
|
|
||||||
|
export const initialState: NoteDetails = {
|
||||||
|
documentContent: '',
|
||||||
|
markdownContent: '',
|
||||||
|
rawFrontmatter: '',
|
||||||
|
frontmatterRendererInfo: {
|
||||||
|
frontmatterInvalid: false,
|
||||||
|
deprecatedSyntax: false,
|
||||||
|
offsetLines: 0
|
||||||
|
},
|
||||||
|
id: '',
|
||||||
|
createTime: DateTime.fromSeconds(0),
|
||||||
|
lastChange: {
|
||||||
|
timestamp: DateTime.fromSeconds(0),
|
||||||
|
userName: ''
|
||||||
|
},
|
||||||
|
alias: '',
|
||||||
|
viewCount: 0,
|
||||||
|
authorship: [],
|
||||||
|
noteTitle: '',
|
||||||
|
firstHeading: '',
|
||||||
|
frontmatter: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
tags: [],
|
||||||
|
deprecatedTagsSyntax: false,
|
||||||
|
robots: '',
|
||||||
|
lang: 'en',
|
||||||
|
dir: NoteTextDirection.LTR,
|
||||||
|
breaks: true,
|
||||||
|
GA: '',
|
||||||
|
disqus: '',
|
||||||
|
type: NoteType.DOCUMENT,
|
||||||
|
opengraph: new Map<string, string>()
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,31 +6,40 @@
|
||||||
|
|
||||||
import { store } from '..'
|
import { store } from '..'
|
||||||
import { NoteDto } from '../../api/notes/types'
|
import { NoteDto } from '../../api/notes/types'
|
||||||
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { initialState } from './reducers'
|
|
||||||
import {
|
import {
|
||||||
NoteDetailsActionType,
|
NoteDetailsActionType,
|
||||||
SetCheckboxInMarkdownContentAction,
|
|
||||||
SetNoteDetailsAction,
|
|
||||||
SetNoteDetailsFromServerAction,
|
SetNoteDetailsFromServerAction,
|
||||||
SetNoteFrontmatterFromRenderingAction,
|
SetNoteDocumentContentAction,
|
||||||
UpdateNoteTitleByFirstHeadingAction
|
UpdateNoteTitleByFirstHeadingAction,
|
||||||
|
UpdateTaskListCheckboxAction
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const setNoteMarkdownContent = (content: string): void => {
|
/**
|
||||||
|
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
|
||||||
|
* @param content The note content as it is written inside the editor pane.
|
||||||
|
*/
|
||||||
|
export const setNoteContent = (content: string): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
|
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
|
||||||
content
|
content: content
|
||||||
} as SetNoteDetailsAction)
|
} as SetNoteDocumentContentAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the note metadata for the current note from an API response DTO to the redux.
|
||||||
|
* @param apiResponse The NoteDTO received from the API to store into redux.
|
||||||
|
*/
|
||||||
export const setNoteDataFromServer = (apiResponse: NoteDto): void => {
|
export const setNoteDataFromServer = (apiResponse: NoteDto): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
|
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
|
||||||
note: apiResponse
|
dto: apiResponse
|
||||||
} as SetNoteDetailsFromServerAction)
|
} as SetNoteDetailsFromServerAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the note title in the redux by the first heading found in the markdown content.
|
||||||
|
* @param firstHeading The content of the first heading found in the markdown content.
|
||||||
|
*/
|
||||||
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
|
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
|
||||||
|
@ -38,20 +47,15 @@ export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
||||||
} as UpdateNoteTitleByFirstHeadingAction)
|
} as UpdateNoteTitleByFirstHeadingAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): void => {
|
/**
|
||||||
if (!frontmatter) {
|
* Changes a checkbox state in the note document content. Triggered when a checkbox in the rendering is clicked.
|
||||||
frontmatter = initialState.frontmatter
|
* @param lineInDocumentContent The line in the document content to change.
|
||||||
}
|
* @param checked true if the checkbox is checked, false otherwise.
|
||||||
|
*/
|
||||||
|
export const setCheckboxInMarkdownContent = (lineInDocumentContent: number, checked: boolean): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER,
|
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX,
|
||||||
frontmatter: frontmatter
|
checkboxChecked: checked,
|
||||||
} as SetNoteFrontmatterFromRenderingAction)
|
changedLine: lineInDocumentContent
|
||||||
}
|
} as UpdateTaskListCheckboxAction)
|
||||||
|
|
||||||
export const setCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
|
|
||||||
store.dispatch({
|
|
||||||
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
|
|
||||||
checked: checked,
|
|
||||||
lineInMarkdown: lineInMarkdown
|
|
||||||
} as SetCheckboxInMarkdownContentAction)
|
|
||||||
}
|
}
|
||||||
|
|
194
src/redux/note-details/reducer.ts
Normal file
194
src/redux/note-details/reducer.ts
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Reducer } from 'redux'
|
||||||
|
import { PresentFrontmatterExtractionResult } from '../../components/common/note-frontmatter/types'
|
||||||
|
import { NoteFrontmatter } from '../../components/common/note-frontmatter/note-frontmatter'
|
||||||
|
import { NoteDetails, NoteDetailsActions, NoteDetailsActionType } from './types'
|
||||||
|
import { extractFrontmatter } from '../../components/common/note-frontmatter/extract-frontmatter'
|
||||||
|
import { NoteDto } from '../../api/notes/types'
|
||||||
|
import { initialState } from './initial-state'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
|
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
||||||
|
state: NoteDetails = initialState,
|
||||||
|
action: NoteDetailsActions
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case NoteDetailsActionType.SET_DOCUMENT_CONTENT:
|
||||||
|
return buildStateFromDocumentContentUpdate(state, action.content)
|
||||||
|
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
|
||||||
|
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
|
||||||
|
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
||||||
|
return buildStateFromServerDto(action.dto)
|
||||||
|
case NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX:
|
||||||
|
return buildStateFromTaskListUpdate(state, action.changedLine, action.checkboxChecked)
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]])( .*)/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
|
||||||
|
* @param dto The first DTO received from the API containing the relevant information about the note.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
|
||||||
|
const newState = convertNoteDtoToNoteDetails(dto)
|
||||||
|
return buildStateFromDocumentContentUpdate(newState, newState.documentContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state where a checkbox in the markdown content either gets checked or unchecked.
|
||||||
|
* @param state The previous redux state.
|
||||||
|
* @param changedLine The number of the line in which the checkbox should be updated.
|
||||||
|
* @param checkboxChecked true if the checkbox should be checked, false otherwise.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
const buildStateFromTaskListUpdate = (
|
||||||
|
state: NoteDetails,
|
||||||
|
changedLine: number,
|
||||||
|
checkboxChecked: boolean
|
||||||
|
): NoteDetails => {
|
||||||
|
const lines = state.documentContent.split('\n')
|
||||||
|
const results = TASK_REGEX.exec(lines[changedLine])
|
||||||
|
if (results) {
|
||||||
|
const before = results[1]
|
||||||
|
const after = results[3]
|
||||||
|
lines[changedLine] = `${before}[${checkboxChecked ? 'x' : ' '}]${after}`
|
||||||
|
return buildStateFromDocumentContentUpdate(state, lines.join('\n'))
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state from a fresh document content.
|
||||||
|
* @param state The previous redux state.
|
||||||
|
* @param documentContent The fresh document content consisting of the frontmatter and markdown part.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
const buildStateFromDocumentContentUpdate = (state: NoteDetails, documentContent: string): NoteDetails => {
|
||||||
|
const frontmatterExtraction = extractFrontmatter(documentContent)
|
||||||
|
if (!frontmatterExtraction.frontmatterPresent) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
documentContent: documentContent,
|
||||||
|
markdownContent: documentContent,
|
||||||
|
rawFrontmatter: '',
|
||||||
|
frontmatter: initialState.frontmatter,
|
||||||
|
frontmatterRendererInfo: initialState.frontmatterRendererInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildStateFromFrontmatterUpdate(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
documentContent: documentContent,
|
||||||
|
markdownContent: documentContent.split('\n').slice(frontmatterExtraction.frontmatterLines).join('\n')
|
||||||
|
},
|
||||||
|
frontmatterExtraction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state from extracted frontmatter data.
|
||||||
|
* @param state The previous redux state.
|
||||||
|
* @param frontmatterExtraction The result of the frontmatter extraction containing the raw data and the line offset.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
const buildStateFromFrontmatterUpdate = (
|
||||||
|
state: NoteDetails,
|
||||||
|
frontmatterExtraction: PresentFrontmatterExtractionResult
|
||||||
|
): NoteDetails => {
|
||||||
|
if (frontmatterExtraction.rawFrontmatterText === state.rawFrontmatter) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const frontmatter = NoteFrontmatter.createFromYaml(frontmatterExtraction.rawFrontmatterText)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
rawFrontmatter: frontmatterExtraction.rawFrontmatterText,
|
||||||
|
frontmatter: frontmatter,
|
||||||
|
noteTitle: generateNoteTitle(frontmatter),
|
||||||
|
frontmatterRendererInfo: {
|
||||||
|
offsetLines: frontmatterExtraction.frontmatterLines,
|
||||||
|
deprecatedSyntax: frontmatter.deprecatedTagsSyntax,
|
||||||
|
frontmatterInvalid: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
rawFrontmatter: frontmatterExtraction.rawFrontmatterText,
|
||||||
|
frontmatter: initialState.frontmatter,
|
||||||
|
frontmatterRendererInfo: {
|
||||||
|
offsetLines: frontmatterExtraction.frontmatterLines,
|
||||||
|
deprecatedSyntax: false,
|
||||||
|
frontmatterInvalid: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link NoteDetails} redux state with an updated note title from frontmatter data and the first heading.
|
||||||
|
* @param state The previous redux state.
|
||||||
|
* @param firstHeading The first heading of the document. Should be {@code undefined} if there is no such heading.
|
||||||
|
* @return An updated {@link NoteDetails} redux state.
|
||||||
|
*/
|
||||||
|
const buildStateFromFirstHeadingUpdate = (state: NoteDetails, firstHeading?: string): NoteDetails => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
firstHeading: firstHeading,
|
||||||
|
noteTitle: generateNoteTitle(state.frontmatter, firstHeading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string) => {
|
||||||
|
if (frontmatter?.title && frontmatter?.title !== '') {
|
||||||
|
return frontmatter.title.trim()
|
||||||
|
} else if (
|
||||||
|
frontmatter?.opengraph &&
|
||||||
|
frontmatter?.opengraph.get('title') &&
|
||||||
|
frontmatter?.opengraph.get('title') !== ''
|
||||||
|
) {
|
||||||
|
return (frontmatter?.opengraph.get('title') ?? firstHeading ?? '').trim()
|
||||||
|
} else {
|
||||||
|
return (firstHeading ?? firstHeading ?? '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a note DTO from the HTTP API to a {@link NoteDetails} object.
|
||||||
|
* Note that the documentContent will be set but the markdownContent and rawFrontmatterContent are yet to be processed.
|
||||||
|
* @param note The NoteDTO as defined in the backend.
|
||||||
|
* @return The NoteDetails object corresponding to the DTO.
|
||||||
|
*/
|
||||||
|
const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
||||||
|
return {
|
||||||
|
documentContent: note.content,
|
||||||
|
markdownContent: '',
|
||||||
|
rawFrontmatter: '',
|
||||||
|
frontmatterRendererInfo: {
|
||||||
|
frontmatterInvalid: false,
|
||||||
|
deprecatedSyntax: false,
|
||||||
|
offsetLines: 0
|
||||||
|
},
|
||||||
|
frontmatter: initialState.frontmatter,
|
||||||
|
id: note.metadata.id,
|
||||||
|
noteTitle: initialState.noteTitle,
|
||||||
|
createTime: DateTime.fromISO(note.metadata.createTime),
|
||||||
|
lastChange: {
|
||||||
|
userName: note.metadata.updateUser.userName,
|
||||||
|
timestamp: DateTime.fromISO(note.metadata.updateTime)
|
||||||
|
},
|
||||||
|
firstHeading: initialState.firstHeading,
|
||||||
|
viewCount: note.metadata.viewCount,
|
||||||
|
alias: note.metadata.alias,
|
||||||
|
authorship: note.metadata.editedBy
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,105 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import { Reducer } from 'redux'
|
|
||||||
import {
|
|
||||||
NoteFrontmatter,
|
|
||||||
NoteTextDirection,
|
|
||||||
NoteType
|
|
||||||
} from '../../components/editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { NoteDetails, NoteDetailsActions, NoteDetailsActionType } from './types'
|
|
||||||
import { noteDtoToNoteDetails } from '../../api/notes/dto-methods'
|
|
||||||
|
|
||||||
export const initialState: NoteDetails = {
|
|
||||||
markdownContent: '',
|
|
||||||
id: '',
|
|
||||||
createTime: DateTime.fromSeconds(0),
|
|
||||||
lastChange: {
|
|
||||||
timestamp: DateTime.fromSeconds(0),
|
|
||||||
userName: ''
|
|
||||||
},
|
|
||||||
alias: '',
|
|
||||||
viewCount: 0,
|
|
||||||
authorship: [],
|
|
||||||
noteTitle: '',
|
|
||||||
firstHeading: '',
|
|
||||||
frontmatter: {
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
tags: [],
|
|
||||||
deprecatedTagsSyntax: false,
|
|
||||||
robots: '',
|
|
||||||
lang: 'en',
|
|
||||||
dir: NoteTextDirection.LTR,
|
|
||||||
breaks: true,
|
|
||||||
GA: '',
|
|
||||||
disqus: '',
|
|
||||||
type: NoteType.DOCUMENT,
|
|
||||||
opengraph: new Map<string, string>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
|
||||||
state: NoteDetails = initialState,
|
|
||||||
action: NoteDetailsActions
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case NoteDetailsActionType.SET_DOCUMENT_CONTENT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
markdownContent: action.content
|
|
||||||
}
|
|
||||||
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
firstHeading: action.firstHeading,
|
|
||||||
noteTitle: generateNoteTitle(state.frontmatter, action.firstHeading)
|
|
||||||
}
|
|
||||||
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
|
|
||||||
return noteDtoToNoteDetails(action.note)
|
|
||||||
case NoteDetailsActionType.SET_NOTE_FRONTMATTER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
frontmatter: action.frontmatter,
|
|
||||||
noteTitle: generateNoteTitle(action.frontmatter, state.firstHeading)
|
|
||||||
}
|
|
||||||
case NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
markdownContent: setCheckboxInMarkdownContent(state.markdownContent, action.lineInMarkdown, action.checked)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]])( .*)/
|
|
||||||
const setCheckboxInMarkdownContent = (markdownContent: string, lineInMarkdown: number, checked: boolean): string => {
|
|
||||||
const lines = markdownContent.split('\n')
|
|
||||||
const results = TASK_REGEX.exec(lines[lineInMarkdown])
|
|
||||||
if (results) {
|
|
||||||
const before = results[1]
|
|
||||||
const after = results[3]
|
|
||||||
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
|
|
||||||
return lines.join('\n')
|
|
||||||
}
|
|
||||||
return markdownContent
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string) => {
|
|
||||||
if (frontmatter?.title && frontmatter?.title !== '') {
|
|
||||||
return frontmatter.title.trim()
|
|
||||||
} else if (
|
|
||||||
frontmatter?.opengraph &&
|
|
||||||
frontmatter?.opengraph.get('title') &&
|
|
||||||
frontmatter?.opengraph.get('title') !== ''
|
|
||||||
) {
|
|
||||||
return (frontmatter?.opengraph.get('title') ?? firstHeading ?? '').trim()
|
|
||||||
} else {
|
|
||||||
return (firstHeading ?? firstHeading ?? '').trim()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,24 +6,30 @@
|
||||||
|
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { Action } from 'redux'
|
import { Action } from 'redux'
|
||||||
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
|
import { NoteFrontmatter } from '../../components/common/note-frontmatter/note-frontmatter'
|
||||||
import { NoteDto } from '../../api/notes/types'
|
import { NoteDto } from '../../api/notes/types'
|
||||||
|
import { RendererFrontmatterInfo } from '../../components/common/note-frontmatter/types'
|
||||||
|
|
||||||
export enum NoteDetailsActionType {
|
export enum NoteDetailsActionType {
|
||||||
SET_DOCUMENT_CONTENT = 'note-details/set',
|
SET_DOCUMENT_CONTENT = 'note-details/content/set',
|
||||||
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
|
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
|
||||||
SET_NOTE_FRONTMATTER = 'note-details/frontmatter/set',
|
|
||||||
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
|
||||||
SET_CHECKBOX_IN_MARKDOWN_CONTENT = 'note-details/toggle-checkbox-in-markdown-content'
|
UPDATE_TASK_LIST_CHECKBOX = 'note-details/update-task-list-checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LastChange {
|
interface LastChange {
|
||||||
userName: string
|
userName: string
|
||||||
timestamp: DateTime
|
timestamp: DateTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redux state containing the currently loaded note with its content and metadata.
|
||||||
|
*/
|
||||||
export interface NoteDetails {
|
export interface NoteDetails {
|
||||||
|
documentContent: string
|
||||||
markdownContent: string
|
markdownContent: string
|
||||||
|
rawFrontmatter: string
|
||||||
|
frontmatter: NoteFrontmatter
|
||||||
|
frontmatterRendererInfo: RendererFrontmatterInfo
|
||||||
id: string
|
id: string
|
||||||
createTime: DateTime
|
createTime: DateTime
|
||||||
lastChange: LastChange
|
lastChange: LastChange
|
||||||
|
@ -32,38 +38,43 @@ export interface NoteDetails {
|
||||||
authorship: string[]
|
authorship: string[]
|
||||||
noteTitle: string
|
noteTitle: string
|
||||||
firstHeading?: string
|
firstHeading?: string
|
||||||
frontmatter: NoteFrontmatter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NoteDetailsActions =
|
export type NoteDetailsActions =
|
||||||
| SetNoteDetailsAction
|
| SetNoteDocumentContentAction
|
||||||
| SetNoteDetailsFromServerAction
|
| SetNoteDetailsFromServerAction
|
||||||
| UpdateNoteTitleByFirstHeadingAction
|
| UpdateNoteTitleByFirstHeadingAction
|
||||||
| SetNoteFrontmatterFromRenderingAction
|
| UpdateTaskListCheckboxAction
|
||||||
| SetCheckboxInMarkdownContentAction
|
|
||||||
|
|
||||||
export interface SetNoteDetailsAction extends Action<NoteDetailsActionType> {
|
/**
|
||||||
|
* Action for updating the document content of the currently loaded note.
|
||||||
|
*/
|
||||||
|
export interface SetNoteDocumentContentAction extends Action<NoteDetailsActionType> {
|
||||||
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT
|
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for overwriting the current state with the data received from the API.
|
||||||
|
*/
|
||||||
export interface SetNoteDetailsFromServerAction extends Action<NoteDetailsActionType> {
|
export interface SetNoteDetailsFromServerAction extends Action<NoteDetailsActionType> {
|
||||||
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
|
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
|
||||||
note: NoteDto
|
dto: NoteDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for updating the note title of the currently loaded note by using frontmatter data or the first heading.
|
||||||
|
*/
|
||||||
export interface UpdateNoteTitleByFirstHeadingAction extends Action<NoteDetailsActionType> {
|
export interface UpdateNoteTitleByFirstHeadingAction extends Action<NoteDetailsActionType> {
|
||||||
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
|
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
|
||||||
firstHeading?: string
|
firstHeading?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetNoteFrontmatterFromRenderingAction extends Action<NoteDetailsActionType> {
|
/**
|
||||||
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER
|
* Action for manipulating the document content of the currently loaded note by changing the checked state of a task list checkbox.
|
||||||
frontmatter: NoteFrontmatter
|
*/
|
||||||
}
|
export interface UpdateTaskListCheckboxAction extends Action<NoteDetailsActionType> {
|
||||||
|
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX
|
||||||
export interface SetCheckboxInMarkdownContentAction extends Action<NoteDetailsActionType> {
|
changedLine: number
|
||||||
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT
|
checkboxChecked: boolean
|
||||||
lineInMarkdown: number
|
|
||||||
checked: boolean
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9508,11 +9508,6 @@ markdown-it-footnote@3.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8"
|
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8"
|
||||||
integrity sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==
|
integrity sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==
|
||||||
|
|
||||||
markdown-it-front-matter@0.2.3:
|
|
||||||
version "0.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.3.tgz#d6fa0f4b362e02086dd4ce8219fadf3f4c9cfa37"
|
|
||||||
integrity sha512-s9+rcClLmZsZc3YL8Awjg/YO/VdphlE20LJ9Bx5a8RAFLI5a1vq6Mll8kOzG6w/wy8yhFLBupaa6Mfd60GATkA==
|
|
||||||
|
|
||||||
markdown-it-imsize@2.0.1:
|
markdown-it-imsize@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz#cca0427905d05338a247cb9ca9d968c5cddd5170"
|
resolved "https://registry.yarnpkg.com/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz#cca0427905d05338a247cb9ca9d968c5cddd5170"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue