Move frontmatter extraction from renderer to redux (#1413)

This commit is contained in:
Erik Michelson 2021-09-02 11:15:31 +02:00 committed by GitHub
parent 7fb7c55877
commit 04e16d8880
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 680 additions and 589 deletions

View file

@ -63,7 +63,6 @@
"markdown-it-deflist": "2.1.0",
"markdown-it-emoji": "2.0.0",
"markdown-it-footnote": "3.0.3",
"markdown-it-front-matter": "0.2.3",
"markdown-it-imsize": "2.0.1",
"markdown-it-ins": "3.0.1",
"markdown-it-mark": "3.0.1",

View file

@ -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
}
}

View file

@ -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')
})
})
})

View file

@ -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
}
}

View file

@ -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')
})
})

View 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)
}
}

View file

@ -4,7 +4,23 @@
* 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 {
title: string | undefined
@ -235,52 +251,3 @@ export enum NoteTextDirection {
LTR = 'ltr',
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>()
}
}

View file

@ -10,7 +10,7 @@ import { useParams } from 'react-router'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
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 { ShowIf } from '../common/show-if/show-if'
import { AppBar, AppBarMode } from '../editor-page/app-bar/app-bar'
@ -32,7 +32,6 @@ export const DocumentReadOnlyPage: React.FC = () => {
useDocumentTitleWithNoteTitle()
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
const [error, loading] = useLoadNoteFromServer()
const markdownContent = useNoteMarkdownContent()
const noteDetails = useApplicationState((state) => state.noteDetails)
@ -60,7 +59,6 @@ export const DocumentReadOnlyPage: React.FC = () => {
frameClasses={'flex-fill h-100 w-100'}
markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onFrontmatterChange={onFrontmatterChange}
rendererType={RendererType.DOCUMENT}
/>
</ShowIf>

View file

@ -14,7 +14,7 @@ import { EditorViewMode } from './editor-view-mode'
import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding'
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 { ReadOnlyModeButton } from './read-only-mode-button'
import { NewNoteButton } from './new-note-button'

View file

@ -13,7 +13,7 @@ import { CopyableField } from '../../../common/copyable/copyable-field/copyable-
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
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'
export interface ShareModalProps {

View file

@ -11,8 +11,7 @@ import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-t
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import {
setCheckboxInMarkdownContent,
setNoteFrontmatter,
setNoteMarkdownContent,
setNoteContent,
updateNoteTitleByFirstHeading
} from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner'
@ -50,6 +49,7 @@ export const EditorPage: React.FC = () => {
const markdownContent = useNoteMarkdownContent()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
const editorMode: EditorMode = useApplicationState((state) => state.editorConfig.editorMode)
const editorSyncScroll: boolean = useApplicationState((state) => state.editorConfig.syncScroll)
@ -98,14 +98,14 @@ export const EditorPage: React.FC = () => {
const leftPane = useMemo(
() => (
<EditorPane
onContentChange={setNoteMarkdownContent}
content={markdownContent}
onContentChange={setNoteContent}
content={documentContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}
/>
),
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
[documentContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
)
const rightPane = useMemo(
@ -116,7 +116,6 @@ export const EditorPage: React.FC = () => {
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
onTaskCheckedChange={setCheckboxInMarkdownContent}
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
scrollState={scrollState.rendererScrollState}
rendererType={RendererType.DOCUMENT}

View file

@ -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')
})
})

View file

@ -25,7 +25,6 @@ export interface RenderIframeProps extends RendererProps {
export const RenderIframe: React.FC<RenderIframeProps> = ({
markdownContent,
onTaskCheckedChange,
onFrontmatterChange,
scrollState,
onFirstHeadingChange,
onScroll,
@ -39,6 +38,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}render`
const resetRendererReady = useCallback(() => setRendererStatus(false), [])
@ -67,11 +67,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
return () => iframeCommunicator.onFirstHeadingChange(undefined)
}, [iframeCommunicator, onFirstHeadingChange])
useEffect(() => {
iframeCommunicator.onFrontmatterChange(onFrontmatterChange)
return () => iframeCommunicator.onFrontmatterChange(undefined)
}, [iframeCommunicator, onFrontmatterChange])
useEffect(() => {
iframeCommunicator.onSetScrollState(onScroll)
return () => iframeCommunicator.onSetScrollState(undefined)
@ -128,6 +123,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
}
}, [iframeCommunicator, markdownContent, rendererReady])
useEffect(() => {
if (rendererReady && frontmatterInfo !== undefined) {
iframeCommunicator.sendSetFrontmatterInfo(frontmatterInfo)
}
}, [iframeCommunicator, rendererReady, frontmatterInfo])
return (
<Fragment>
<ShowOnPropChangeImageLightbox details={lightboxDetails} />

View file

@ -10,14 +10,13 @@ import { Trans, useTranslation } from 'react-i18next'
import links from '../../../links.json'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
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()
const yamlDeprecatedTags = useApplicationState((state) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
return (
<ShowIf condition={yamlDeprecatedTags}>
<ShowIf condition={!!show}>
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
<span className={'text-wrap'}>
<span className={'text-wrap'}>

View file

@ -8,17 +8,17 @@ import React, { useCallback } from 'react'
import sanitize from 'sanitize-filename'
import { store } from '../../../redux'
import { Trans, useTranslation } from 'react-i18next'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { download } from '../../common/download/download'
import { SidebarButton } from './sidebar-button'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ExportMarkdownSidebarEntry: React.FC = () => {
const { t } = useTranslation()
const markdownContent = useNoteMarkdownContent()
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
const onClick = useCallback(() => {
const sanitized = sanitize(store.getState().noteDetails.noteTitle)
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
}, [markdownContent, t])
download(documentContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
}, [documentContent, t])
return (
<SidebarButton data-cy={'menu-export-markdown'} onClick={onClick} icon={'file-text'}>

View file

@ -7,7 +7,7 @@
import React, { Fragment, useCallback, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
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 { UploadInput } from './upload-input'
@ -21,7 +21,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const fileReader = new FileReader()
fileReader.addEventListener('load', () => {
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', () => {
resolve()

View file

@ -4,7 +4,7 @@
* 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 { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
@ -12,7 +12,6 @@ import { ComponentReplacer } from './replace-components/ComponentReplacer'
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
import { useComponentReplacers } from './hooks/use-component-replacers'
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 { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
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 { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
import { ImageClickHandler } from './replace-components/image/image-replacer'
import { InvalidYamlAlert } from './invalid-yaml-alert'
import { useTrimmedContent } from './hooks/use-trimmed-content'
export interface BasicMarkdownRendererProps {
@ -29,79 +27,57 @@ export interface BasicMarkdownRendererProps {
onAfterRendering?: () => void
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast?: TocAst) => void
baseUrl?: string
onImageClick?: ImageClickHandler
outerContainerRef?: Ref<HTMLDivElement>
useAlternativeBreaks?: boolean
frontmatterLineOffset?: number
}
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
className,
content,
additionalReplacers,
onBeforeRendering,
onAfterRendering,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onFrontmatterChange,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
outerContainerRef,
useAlternativeBreaks
useAlternativeBreaks,
frontmatterLineOffset
}) => {
const rawMetaRef = useRef<RawNoteFrontmatter>()
const markdownBodyRef = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
const hasNewYamlError = useRef(false)
const tocAst = useRef<TocAst>()
const [showYamlError, setShowYamlError] = useState(false)
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
const markdownIt = useMemo(
() =>
new BasicMarkdownItConfigurator({
useFrontmatter: !!onFrontmatterChange,
onParseError: (errorState) => (hasNewYamlError.current = errorState),
onRawMetaChange: (rawMeta) => (rawMetaRef.current = rawMeta),
onToc: (toc) => (tocAst.current = toc),
onLineMarkers:
onLineMarkerPositionChanged === undefined
? undefined
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
useAlternativeBreaks
useAlternativeBreaks,
offsetLines: frontmatterLineOffset
}).buildConfiguredMarkdownIt(),
[onFrontmatterChange, onLineMarkerPositionChanged, useAlternativeBreaks]
[onLineMarkerPositionChanged, useAlternativeBreaks, frontmatterLineOffset]
)
const clearFrontmatter = useCallback(() => {
hasNewYamlError.current = false
rawMetaRef.current = undefined
onBeforeRendering?.()
}, [onBeforeRendering])
const checkYamlErrorState = useCallback(() => {
setShowYamlError(hasNewYamlError.current)
onAfterRendering?.()
}, [onAfterRendering])
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl)
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, frontmatterLineOffset)
const replacers = useCallback(
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
[additionalReplacers, baseReplacers]
)
const markdownReactDom = useConvertMarkdownToReactDom(
trimmedContent,
markdownIt,
replacers,
clearFrontmatter,
checkYamlErrorState
)
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers)
useTranslation()
useCalculateLineMarkerPosition(
@ -112,17 +88,9 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
)
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
useOnRefChange(tocAst, onTocChange)
useOnRefChange(rawMetaRef, (newValue) => {
if (!newValue) {
onFrontmatterChange?.(undefined)
} else {
onFrontmatterChange?.(new NoteFrontmatter(newValue))
}
})
return (
<div ref={outerContainerRef} className={'position-relative'}>
<InvalidYamlAlert show={showYamlError} />
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
<div
ref={markdownBodyRef}

View file

@ -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 onImageClick A callback that should be executed if an image gets clicked
* @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
*/
export const useComponentReplacers = (
onTaskCheckedChange?: TaskCheckedChangeHandler,
onImageClick?: ImageClickHandler,
baseUrl?: string
baseUrl?: string,
frontmatterLinesToSkip?: number
): (() => ComponentReplacer[]) =>
useCallback(
() => [
@ -59,8 +61,8 @@ export const useComponentReplacers = (
new HighlightedCodeReplacer(),
new ColoredBlockquoteReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange),
new TaskListReplacer(onTaskCheckedChange, frontmatterLinesToSkip),
new LinkReplacer(baseUrl)
],
[onImageClick, onTaskCheckedChange, baseUrl]
[onImageClick, onTaskCheckedChange, baseUrl, frontmatterLinesToSkip]
)

View file

@ -19,7 +19,6 @@ import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
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 { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
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 { quoteExtra } from '../markdown-it-plugins/quote-extra'
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
export interface ConfiguratorDetails {
useFrontmatter: boolean
onParseError: (error: boolean) => void
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void
onToc: (toc: TocAst) => void
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
useAlternativeBreaks?: boolean
offsetLines?: number
}
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
@ -105,17 +102,8 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
spoilerContainer
)
if (this.options.useFrontmatter) {
this.configurations.push(
frontmatterExtract({
onParseError: this.options.onParseError,
onRawMetaChange: this.options.onRawMetaChange
})
)
}
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)

View file

@ -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)
}
})
}

View file

@ -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.
* It also provides a list of line numbers for the top level dom elements.
*/
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple =
(options) => (md: MarkdownIt) => {
export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: number) => MarkdownIt.PluginSimple =
(options, offsetLines = 0) =>
(md: MarkdownIt) => {
// add app_linemarker token before each opening or self-closing level-0 tag
md.core.ruler.push('line_number_marker', (state) => {
const lineMarkers: LineMarkers[] = []
tagTokens(state.tokens, lineMarkers)
tagTokens(state.tokens, lineMarkers, offsetLines)
if (options) {
options(lineMarkers)
}
@ -56,7 +57,7 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
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++) {
const token = tokens[tokenPosition]
if (token.hidden) {
@ -71,14 +72,14 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
const endLineNumber = token.map[1] + 1
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)
tokenPosition += 1
if (token.children) {
tagTokens(token.children, lineMarkers)
tagTokens(token.children, lineMarkers, offsetLines)
}
}
}

View file

@ -15,16 +15,18 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
*/
export class TaskListReplacer extends ComponentReplacer {
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
private readonly frontmatterLinesOffset
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler, frontmatterLinesOffset?: number) {
super()
this.onTaskCheckedChange = onTaskCheckedChange
this.frontmatterLinesOffset = frontmatterLinesOffset ?? 0
}
handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const lineNum = Number(event.currentTarget.dataset.line)
if (this.onTaskCheckedChange) {
this.onTaskCheckedChange(lineNum, event.currentTarget.checked)
this.onTaskCheckedChange(lineNum + this.frontmatterLinesOffset, event.currentTarget.checked)
}
}

View file

@ -3,8 +3,6 @@
*
* 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 { IframeCommunicator } from './iframe-communicator'
import {
@ -14,6 +12,7 @@ import {
RendererToEditorIframeMessage,
RenderIframeMessageType
} from './rendering-message'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export class IframeEditorToRendererCommunicator extends IframeCommunicator<
EditorToRendererIframeMessage,
@ -22,7 +21,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
private onSetScrollSourceToRendererHandler?: () => void
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
private onFirstHeadingChangeHandler?: (heading?: string) => void
private onFrontmatterChangeHandler?: (frontmatter?: NoteFrontmatter) => void
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onRendererReadyHandler?: () => void
private onImageClickedHandler?: (details: ImageDetails) => void
@ -33,10 +31,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
this.onHeightChangeHandler = handler
}
public onFrontmatterChange(handler?: (frontmatter?: NoteFrontmatter) => void): void {
this.onFrontmatterChangeHandler = handler
}
public onImageClicked(handler?: (details: ImageDetails) => void): void {
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 {
const renderMessage = event.data
switch (renderMessage.type) {
@ -121,9 +122,6 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_FRONTMATTER:
this.onFrontmatterChangeHandler?.(renderMessage.frontmatter)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false

View file

@ -8,18 +8,22 @@ import React, { useCallback, useEffect, useState } from 'react'
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
import { BaseConfiguration, RendererType } from './rendering-message'
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 { useImageClickHandler } from './hooks/use-image-click-handler'
import { MarkdownDocument } from './markdown-document'
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
import { countWords } from './word-counter'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export const IframeMarkdownRenderer: React.FC = () => {
const [markdownContent, setMarkdownContent] = useState('')
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>({
offsetLines: 0,
frontmatterInvalid: false,
deprecatedSyntax: false
})
const iframeCommunicator = useIFrameRendererToEditorCommunicator()
@ -37,6 +41,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
useEffect(() => iframeCommunicator.onSetFrontmatterInfo(setFrontmatterInfo), [iframeCommunicator, setFrontmatterInfo])
useEffect(
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
[iframeCommunicator, countWordsInRenderedDocument]
@ -60,14 +65,6 @@ export const IframeMarkdownRenderer: React.FC = () => {
iframeCommunicator.sendSetScrollSourceToRenderer()
}, [iframeCommunicator])
const onFrontmatterChange = useCallback(
(frontmatter?: NoteFrontmatter) => {
setNoteFrontmatter(frontmatter)
iframeCommunicator.sendSetFrontmatter(frontmatter)
},
[iframeCommunicator]
)
const onScroll = useCallback(
(scrollState: ScrollState) => {
iframeCommunicator.sendSetScrollState(scrollState)
@ -97,11 +94,11 @@ export const IframeMarkdownRenderer: React.FC = () => {
onTaskCheckedChange={onTaskCheckedChange}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource}
onFrontmatterChange={onFrontmatterChange}
scrollState={scrollState}
onScroll={onScroll}
baseUrl={baseConfiguration.baseUrl}
onImageClick={onImageClick}
frontmatterInfo={frontmatterInfo}
/>
)
case RendererType.INTRO:

View file

@ -4,7 +4,6 @@
* 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 { IframeCommunicator } from './iframe-communicator'
import {
@ -14,6 +13,7 @@ import {
RendererToEditorIframeMessage,
RenderIframeMessageType
} from './rendering-message'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export class IframeRendererToEditorCommunicator extends IframeCommunicator<
RendererToEditorIframeMessage,
@ -24,6 +24,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
private onGetWordCountHandler?: () => void
private onSetFrontmatterInfoHandler?: (frontmatterInfo: RendererFrontmatterInfo) => void
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
this.onSetBaseConfigurationHandler = handler
@ -45,6 +46,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
this.onGetWordCountHandler = handler
}
public onSetFrontmatterInfo(handler?: (frontmatterInfo: RendererFrontmatterInfo) => void): void {
this.onSetFrontmatterInfoHandler = handler
}
public sendRendererReady(): void {
this.enableCommunication()
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 {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_STATE,
@ -126,6 +124,9 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
case RenderIframeMessageType.GET_WORD_COUNT:
this.onGetWordCountHandler?.()
return false
case RenderIframeMessageType.SET_FRONTMATTER_INFO:
this.onSetFrontmatterInfoHandler?.(renderMessage.frontmatterInfo)
return false
}
}
}

View file

@ -7,7 +7,6 @@
import { TocAst } from 'markdown-it-toc-done-right'
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
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 { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
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 { ShowIf } from '../common/show-if/show-if'
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 {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
markdownContent: string
@ -33,13 +33,13 @@ export interface MarkdownDocumentProps extends RendererProps {
additionalRendererClasses?: string
disableToc?: boolean
baseUrl: string
frontmatterInfo?: RendererFrontmatterInfo
}
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
additionalOuterContainerClasses,
additionalRendererClasses,
onFirstHeadingChange,
onFrontmatterChange,
onMakeScrollSource,
onTaskCheckedChange,
baseUrl,
@ -48,7 +48,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
onScroll,
scrollState,
onHeightChange,
disableToc
disableToc,
frontmatterInfo
}) => {
const rendererRef = useRef<HTMLDivElement | null>(null)
const rendererSize = useResizeObserver({ ref: rendererRef.current })
@ -85,19 +86,20 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
onMouseEnter={onMakeScrollSource}>
<div className={'markdown-document-side'} />
<div className={'markdown-document-content'}>
<YamlArrayDeprecationAlert />
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
<BasicMarkdownRenderer
outerContainerRef={rendererRef}
className={`mb-3 ${additionalRendererClasses ?? ''}`}
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
onFrontmatterChange={onFrontmatterChange}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={setTocAst}
baseUrl={baseUrl}
onImageClick={onImageClick}
useAlternativeBreaks={useAlternativeBreaks}
frontmatterLineOffset={frontmatterInfo?.offsetLines}
/>
</div>
<div className={'markdown-document-side pt-4'}>

View file

@ -3,8 +3,8 @@
*
* 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 { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export enum RenderIframeMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
@ -14,12 +14,12 @@ export enum RenderIframeMessageType {
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
IMAGE_CLICKED = 'IMAGE_CLICKED',
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
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 {
@ -72,9 +72,9 @@ export interface OnFirstHeadingChangeMessage {
firstHeading: string | undefined
}
export interface OnFrontmatterChangeMessage {
type: RenderIframeMessageType.ON_SET_FRONTMATTER
frontmatter: NoteFrontmatter | undefined
export interface SetFrontmatterInfoMessage {
type: RenderIframeMessageType.SET_FRONTMATTER_INFO
frontmatterInfo: RendererFrontmatterInfo
}
export interface OnHeightChangeMessage {
@ -93,12 +93,12 @@ export type EditorToRendererIframeMessage =
| SetScrollStateMessage
| SetBaseUrlMessage
| GetWordCountMessage
| SetFrontmatterInfoMessage
export type RendererToEditorIframeMessage =
| RendererToEditorSimpleMessage
| OnFirstHeadingChangeMessage
| OnTaskCheckboxChangeMessage
| OnFrontmatterChangeMessage
| SetScrollStateMessage
| ImageClickedMessage
| OnHeightChangeMessage

View file

@ -15,7 +15,7 @@ import { DarkModeConfigReducer } from './dark-mode/reducers'
import { DarkModeConfig } from './dark-mode/types'
import { EditorConfigReducer } from './editor/reducers'
import { EditorConfig } from './editor/types'
import { NoteDetailsReducer } from './note-details/reducers'
import { NoteDetailsReducer } from './note-details/reducer'
import { NoteDetails } from './note-details/types'
import { UserReducer } from './user/reducers'
import { OptionalUserState } from './user/types'

View 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>()
}
}

View file

@ -6,31 +6,40 @@
import { store } from '..'
import { NoteDto } from '../../api/notes/types'
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
import { initialState } from './reducers'
import {
NoteDetailsActionType,
SetCheckboxInMarkdownContentAction,
SetNoteDetailsAction,
SetNoteDetailsFromServerAction,
SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction
SetNoteDocumentContentAction,
UpdateNoteTitleByFirstHeadingAction,
UpdateTaskListCheckboxAction
} 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({
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
content
} as SetNoteDetailsAction)
content: content
} 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 => {
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
note: apiResponse
dto: apiResponse
} 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 => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
@ -38,20 +47,15 @@ export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
} as UpdateNoteTitleByFirstHeadingAction)
}
export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): void => {
if (!frontmatter) {
frontmatter = initialState.frontmatter
}
/**
* Changes a checkbox state in the note document content. Triggered when a checkbox in the rendering is clicked.
* @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({
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER,
frontmatter: frontmatter
} as SetNoteFrontmatterFromRenderingAction)
}
export const setCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
store.dispatch({
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
checked: checked,
lineInMarkdown: lineInMarkdown
} as SetCheckboxInMarkdownContentAction)
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX,
checkboxChecked: checked,
changedLine: lineInDocumentContent
} as UpdateTaskListCheckboxAction)
}

View 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
}
}

View file

@ -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()
}
}

View file

@ -6,24 +6,30 @@
import { DateTime } from 'luxon'
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 { RendererFrontmatterInfo } from '../../components/common/note-frontmatter/types'
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_FRONTMATTER = 'note-details/frontmatter/set',
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 {
userName: string
timestamp: DateTime
}
/**
* Redux state containing the currently loaded note with its content and metadata.
*/
export interface NoteDetails {
documentContent: string
markdownContent: string
rawFrontmatter: string
frontmatter: NoteFrontmatter
frontmatterRendererInfo: RendererFrontmatterInfo
id: string
createTime: DateTime
lastChange: LastChange
@ -32,38 +38,43 @@ export interface NoteDetails {
authorship: string[]
noteTitle: string
firstHeading?: string
frontmatter: NoteFrontmatter
}
export type NoteDetailsActions =
| SetNoteDetailsAction
| SetNoteDocumentContentAction
| SetNoteDetailsFromServerAction
| UpdateNoteTitleByFirstHeadingAction
| SetNoteFrontmatterFromRenderingAction
| SetCheckboxInMarkdownContentAction
| UpdateTaskListCheckboxAction
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
content: string
}
/**
* Action for overwriting the current state with the data received from the API.
*/
export interface SetNoteDetailsFromServerAction extends Action<NoteDetailsActionType> {
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> {
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
firstHeading?: string
}
export interface SetNoteFrontmatterFromRenderingAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER
frontmatter: NoteFrontmatter
}
export interface SetCheckboxInMarkdownContentAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT
lineInMarkdown: number
checked: boolean
/**
* Action for manipulating the document content of the currently loaded note by changing the checked state of a task list checkbox.
*/
export interface UpdateTaskListCheckboxAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX
changedLine: number
checkboxChecked: boolean
}

View file

@ -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"
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:
version "2.0.1"
resolved "https://registry.yarnpkg.com/markdown-it-imsize/-/markdown-it-imsize-2.0.1.tgz#cca0427905d05338a247cb9ca9d968c5cddd5170"