mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-03-15 04:31:56 +00: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-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",
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
||||
// 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>()
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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> = ({
|
||||
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} />
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
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 { 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)
|
||||
}
|
||||
|
|
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 { 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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue