diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5bf0e5..da14a2cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Please visit https://github.com/shd101wyy/vscode-markdown-preview-enhanced/relea ## [Unreleased] +## [0.9.7] - 2023-12-10 + +### New features + +- Added `enablePreviewZenMode` option and reorganized the right-click context menu. + + ![image](https://github.com/shd101wyy/crossnote/assets/1908863/26e2237e-c6e2-433e-a063-6de2c01a64bb) + +### Bug fixes + +- Fixed rendering `vega-lite` in `Reveal.js` slide: https://github.com/shd101wyy/vscode-markdown-preview-enhanced/issues/1880 +- Removed one github-dark background css attribute: https://github.com/shd101wyy/crossnote/issues/344 + ## [0.9.6] - 2023-10-24 ### Changes diff --git a/README.md b/README.md index e3ad031b..e079ef49 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ const config = { // In Markdown, a single newline character doesn't cause a line break in the generated HTML. In GitHub Flavored Markdown, that is not true. Enable this config option to insert line breaks in rendered HTML for single newlines in Markdown source. breakOnSingleNewLine: true, + // Whether to enable preview zen mode. + // Enable this option will hide unnecessary UI elements in preview unless your mouse is over it. + enablePreviewZenMode: boolean; + // Enable smartypants and other sweet transforms. enableTypographer: false, diff --git a/package.json b/package.json index ea8da0bb..b320c831 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crossnote", - "version": "0.9.6", + "version": "0.9.7", "description": "A powerful markdown notebook tool", "keywords": [ "markdown" diff --git a/src/markdown-engine/index.ts b/src/markdown-engine/index.ts index 070bd03e..9486ef03 100644 --- a/src/markdown-engine/index.ts +++ b/src/markdown-engine/index.ts @@ -360,6 +360,9 @@ if (typeof(window['Reveal']) !== 'undefined') { } for (var i = 0; i < vegaEls.length; i++) { const vegaEl = vegaEls[i] + if (vegaEl.hasAttribute("data-processed")) { + continue; + } try { var spec = JSON.parse(vegaEl.textContent); vegaEmbed(vegaEl, spec, { actions: false, renderer: 'svg' }) diff --git a/src/markdown-engine/transformer.ts b/src/markdown-engine/transformer.ts index 6ae015d5..da672854 100644 --- a/src/markdown-engine/transformer.ts +++ b/src/markdown-engine/transformer.ts @@ -693,7 +693,7 @@ export async function transformMarkdown( } // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#common_image_file_types else if ( - extname.match(/^\.(apng|avif|gif|jpeg|jpg|png|svg|bmp|webp)/) || + extname.match(/^\.(apng|avif|gif|jpeg|jpg|png|svg|bmp|webp|emf)/) || extname === '' // NOTE: For example, for github image like: ![Screenshot from 2023-10-15 15-34-27](https://github.com/shd101wyy/crossnote/assets/1908863/ede91390-3cca-4b83-8e30-33027bf0a363) ) { if (importMatch || wikilinkImportMatch) { diff --git a/src/notebook/types.ts b/src/notebook/types.ts index 247ec131..2eb0aa9e 100644 --- a/src/notebook/types.ts +++ b/src/notebook/types.ts @@ -193,6 +193,12 @@ export interface NotebookConfig { */ parserConfig: ParserConfig; + /** + * Whether to enable preview zen mode. + * Enable this option will hide unnecessary UI elements in preview unless your mouse is over it. + */ + enablePreviewZenMode: boolean; + /** * Whether to break on single new line. * @@ -534,6 +540,7 @@ export function getDefaultNotebookConfig(): NotebookConfig { mathjaxConfig: getDefaultMathjaxConfig(), katexConfig: getDefaultKatexConfig(), parserConfig: getDefaultParserConfig(), + enablePreviewZenMode: false, usePandocParser: false, breakOnSingleNewLine: true, enableTypographer: false, diff --git a/src/webview/components/ContextMenu.tsx b/src/webview/components/ContextMenu.tsx index 2b865bf6..b31b30aa 100644 --- a/src/webview/components/ContextMenu.tsx +++ b/src/webview/components/ContextMenu.tsx @@ -1,6 +1,5 @@ import { mdiCancel, - mdiExport, mdiExportVariant, mdiImageOutline, mdiInformationOutline, @@ -9,9 +8,11 @@ import { mdiOpenInNew, mdiPaletteOutline, mdiPencil, + mdiSpaOutline, mdiSync, } from '@mdi/js'; import Icon from '@mdi/react'; +import classNames from 'classnames'; import React, { useCallback } from 'react'; import { Item, ItemParams, Menu, Separator, Submenu } from 'react-contexify'; import 'react-contexify/ReactContexify.css'; @@ -32,6 +33,7 @@ export default function ContextMenu() { sourceUri, theme, isPresentationMode, + enablePreviewZenMode, } = PreviewContainer.useContainer(); const handleItemClick = useCallback( @@ -89,6 +91,10 @@ export default function ContextMenu() { postMessage('markdownExport', [sourceUri.current]); break; } + case 'toggle-zen-mode': { + postMessage('togglePreviewZenMode', [sourceUri.current]); + break; + } case 'open-image-helper': { setShowImageHelper(true); break; @@ -215,105 +221,72 @@ export default function ContextMenu() { size={0.8} className="mr-2" > - HTML + Export } > - - {'HTML (offline)'} - - - {'HTML (cdn hosted)'} - - - )} - {!isVSCodeWebExtension && ( - HTML + } + > + + {'HTML (offline)'} + + + {'HTML (cdn hosted)'} + + + + Chrome (Puppeteer) + + } + > + + PDF + + + PNG + + + JPEG + + + - - Chrome (Puppeteer) + PDF (Prince) - } - > - - PDF - - PNG - - - JPEG + eBook + } + > + + ePub + + + Mobi + + + PDF + + + HTML + + + + Pandoc - - )} - {!isVSCodeWebExtension && ( - - - - PDF (Prince) - - - )} - {!isVSCodeWebExtension && ( - - - eBook + Save as Markdown - } - > - - ePub - - - Mobi - - - PDF - - - HTML )} - {!isVSCodeWebExtension && ( - - - - Pandoc - - - )} - {!isVSCodeWebExtension && ( - - - - Save as Markdown - - - )} {!isVSCodeWebExtension && } )} + + + + + Zen Mode + + + {!isVSCodeWebExtension && ( <> - diff --git a/src/webview/components/Footer.tsx b/src/webview/components/Footer.tsx index 0f3dd14e..67b87d65 100644 --- a/src/webview/components/Footer.tsx +++ b/src/webview/components/Footer.tsx @@ -15,6 +15,7 @@ export default function Footer() { showBacklinks, theme, markdown, + enablePreviewZenMode, } = PreviewContainer.useContainer(); const [readingTimeEstimation, setReadingTimeEstimation] = useState< | { @@ -39,12 +40,15 @@ export default function Footer() { data-theme={theme} >
- {readingTimeEstimation && ( + {!enablePreviewZenMode && readingTimeEstimation && (
{readingTimeEstimation.text}
diff --git a/src/webview/components/Preview.tsx b/src/webview/components/Preview.tsx index eafa79a8..0a20db3b 100644 --- a/src/webview/components/Preview.tsx +++ b/src/webview/components/Preview.tsx @@ -14,6 +14,7 @@ import { Topbar } from './Topbar'; export default function Preview() { const { + enablePreviewZenMode, hiddenPreviewElement, isPresentationMode, isLoadingPreview, @@ -80,9 +81,11 @@ export default function Preview() { {/** Context menu */} {/** Floating Actions */} - + {!enablePreviewZenMode && } {/** Markdown Editor */} - {highlightElementBeingEdited && } + {!enablePreviewZenMode && highlightElementBeingEdited && ( + + )}
); } diff --git a/src/webview/containers/preview.ts b/src/webview/containers/preview.ts index 25b0fa87..d162ae82 100644 --- a/src/webview/containers/preview.ts +++ b/src/webview/containers/preview.ts @@ -163,6 +163,9 @@ const PreviewContainer = createContainer(() => { const isVSCode = useMemo(() => { return !!config.isVSCode; }, [config]); + const enablePreviewZenMode = useMemo(() => { + return !!config.enablePreviewZenMode; + }, [config]); const contextMenuId = useMemo(() => { return 'crossnote-context-menu'; }, []); @@ -862,120 +865,128 @@ const PreviewContainer = createContainer(() => { } }, [postMessage]); - const bindHighlightEvent = useCallback((previewElement: HTMLDivElement) => { - setHighlightElement(null); - const sourceLineElements = - previewElement.querySelectorAll('[data-source-line]'); - - const highlightElementsThatAddedEventSet = new Set(); - const sourceLineElementToContainerElementMap = new Map< - Element | HTMLElement, - Element | HTMLElement - >(); - - highlightElementToLinesMap.current = new Map< - HTMLElement | Element, - number[] - >(); - highlightElementLines.current = []; - const startLinesSet = new Set(); - - const bindHighlightElementsEvent = ( - highlightElements: (HTMLElement | Element)[], - startLine: number, - ) => { - startLinesSet.add(startLine); - - if (highlightElements.length === 0) { + const bindHighlightEvent = useCallback( + (previewElement: HTMLDivElement) => { + // NOTE: No need to handle this event in zen mode. + if (enablePreviewZenMode) { return; } - // console.log('* highlightElements: ', highlightElements); - const firstHighlightElement = highlightElements[0] as HTMLElement; - const linesSet = new Set([startLine]); - - // Iterate over highlightElementToLinesMap - // If firstHighlightElement contains the highlightElement in the map - // Add its lines to the current linesSet - for (const [ - highlightElement, - lines, - ] of highlightElementToLinesMap.current) { - if (firstHighlightElement.contains(highlightElement)) { - lines.forEach((line) => { - linesSet.add(line); - }); - } - } - // Add event listeners - highlightElements.forEach((highlightElement) => { - if (highlightElementsThatAddedEventSet.has(highlightElement)) { - const lines = - highlightElementToLinesMap.current.get(highlightElement); - if (lines) { + setHighlightElement(null); + const sourceLineElements = + previewElement.querySelectorAll('[data-source-line]'); + + const highlightElementsThatAddedEventSet = new Set< + Element | HTMLElement + >(); + const sourceLineElementToContainerElementMap = new Map< + Element | HTMLElement, + Element | HTMLElement + >(); + + highlightElementToLinesMap.current = new Map< + HTMLElement | Element, + number[] + >(); + highlightElementLines.current = []; + const startLinesSet = new Set(); + + const bindHighlightElementsEvent = ( + highlightElements: (HTMLElement | Element)[], + startLine: number, + ) => { + startLinesSet.add(startLine); + + if (highlightElements.length === 0) { + return; + } + // console.log('* highlightElements: ', highlightElements); + const firstHighlightElement = highlightElements[0] as HTMLElement; + const linesSet = new Set([startLine]); + + // Iterate over highlightElementToLinesMap + // If firstHighlightElement contains the highlightElement in the map + // Add its lines to the current linesSet + for (const [ + highlightElement, + lines, + ] of highlightElementToLinesMap.current) { + if (firstHighlightElement.contains(highlightElement)) { lines.forEach((line) => { linesSet.add(line); }); } - return; - } else { - highlightElementsThatAddedEventSet.add(highlightElement); } - highlightElement.addEventListener( - /*'mouseenter'*/ 'mouseover', - (event) => { - event.stopPropagation(); - setIsMouseOverPreview(true); - - // Remove "highlight-line" class name from all highlight elements. - const currentHighlightElements = Array.from( - document.getElementsByClassName('highlight-line'), - ); - currentHighlightElements.forEach((currentHighlightElement) => { - currentHighlightElement.classList.remove('highlight-line'); - }); - // Add "highlight-line" class name. - highlightElements.forEach((highlightElement) => { - highlightElement.classList.add('highlight-line'); - }); - setHighlightElement(firstHighlightElement); - }, - ); - highlightElement.addEventListener( - /*'mouseleave'*/ 'mouseout', - (event) => { - event.stopPropagation(); - highlightElements.forEach((highlightElement) => { - highlightElement.classList.remove('highlight-line'); - highlightElement.classList.remove('highlight-active'); - }); - setHighlightElement(null); - }, + // Add event listeners + highlightElements.forEach((highlightElement) => { + if (highlightElementsThatAddedEventSet.has(highlightElement)) { + const lines = + highlightElementToLinesMap.current.get(highlightElement); + if (lines) { + lines.forEach((line) => { + linesSet.add(line); + }); + } + return; + } else { + highlightElementsThatAddedEventSet.add(highlightElement); + } + highlightElement.addEventListener( + /*'mouseenter'*/ 'mouseover', + (event) => { + event.stopPropagation(); + setIsMouseOverPreview(true); + + // Remove "highlight-line" class name from all highlight elements. + const currentHighlightElements = Array.from( + document.getElementsByClassName('highlight-line'), + ); + currentHighlightElements.forEach((currentHighlightElement) => { + currentHighlightElement.classList.remove('highlight-line'); + }); + + // Add "highlight-line" class name. + highlightElements.forEach((highlightElement) => { + highlightElement.classList.add('highlight-line'); + }); + setHighlightElement(firstHighlightElement); + }, + ); + highlightElement.addEventListener( + /*'mouseleave'*/ 'mouseout', + (event) => { + event.stopPropagation(); + highlightElements.forEach((highlightElement) => { + highlightElement.classList.remove('highlight-line'); + highlightElement.classList.remove('highlight-active'); + }); + setHighlightElement(null); + }, + ); + }); + highlightElementToLinesMap.current.set( + firstHighlightElement, + Array.from(linesSet).sort((a, b) => a - b), ); - }); - highlightElementToLinesMap.current.set( - firstHighlightElement, - Array.from(linesSet).sort((a, b) => a - b), - ); - }; + }; - for (let i = sourceLineElements.length - 1; i >= 0; i--) { - const sourceLineElement = sourceLineElements[i]; - const dataSourceLine = getDataSourceLine(sourceLineElement) ?? 0; + for (let i = sourceLineElements.length - 1; i >= 0; i--) { + const sourceLineElement = sourceLineElements[i]; + const dataSourceLine = getDataSourceLine(sourceLineElement) ?? 0; - if (dataSourceLine > totalLineCount.current) { - // FIXME: This means we didn't get the source map correctly. - return; - } + if (dataSourceLine > totalLineCount.current) { + // FIXME: This means we didn't get the source map correctly. + return; + } - // Ignore the link - if (sourceLineElement.tagName === 'A') { - continue; - } + // Ignore the link + if (sourceLineElement.tagName === 'A') { + continue; + } - // Input List item - /* + // Input List item + /* if ( sourceLineElement.tagName === 'INPUT' && sourceLineElement.classList.contains('task-list-item-checkbox') @@ -997,112 +1008,119 @@ const PreviewContainer = createContainer(() => { } } */ - // Code chunk - if ( - sourceLineElement.tagName === 'PRE' && - sourceLineElement.parentElement?.classList.contains('input-div') && - sourceLineElement.parentElement.parentElement?.classList.contains( - 'code-chunk', - ) - ) { - const highlightElement = sourceLineElement.parentElement.parentElement; - if (highlightElement) { - bindHighlightElementsEvent([highlightElement], dataSourceLine); + // Code chunk + if ( + sourceLineElement.tagName === 'PRE' && + sourceLineElement.parentElement?.classList.contains('input-div') && + sourceLineElement.parentElement.parentElement?.classList.contains( + 'code-chunk', + ) + ) { + const highlightElement = + sourceLineElement.parentElement.parentElement; + if (highlightElement) { + bindHighlightElementsEvent([highlightElement], dataSourceLine); - sourceLineElementToContainerElementMap.set( - sourceLineElement, - highlightElement, - ); + sourceLineElementToContainerElementMap.set( + sourceLineElement, + highlightElement, + ); + } } - } - // Image and link - else if ( - sourceLineElement.tagName === 'IMG' || - sourceLineElement.tagName === 'A' - ) { - const highlightElement = sourceLineElement.parentElement; - if (highlightElement) { - bindHighlightElementsEvent([highlightElement], dataSourceLine); + // Image and link + else if ( + sourceLineElement.tagName === 'IMG' || + sourceLineElement.tagName === 'A' + ) { + const highlightElement = sourceLineElement.parentElement; + if (highlightElement) { + bindHighlightElementsEvent([highlightElement], dataSourceLine); + + sourceLineElementToContainerElementMap.set( + sourceLineElement, + highlightElement, + ); + } + } + // Other elements + else { + bindHighlightElementsEvent([sourceLineElement], dataSourceLine); sourceLineElementToContainerElementMap.set( sourceLineElement, - highlightElement, + sourceLineElement.parentElement?.tagName === 'P' + ? sourceLineElement.parentElement + : sourceLineElement, ); } - } - // Other elements - else { - bindHighlightElementsEvent([sourceLineElement], dataSourceLine); - - sourceLineElementToContainerElementMap.set( - sourceLineElement, - sourceLineElement.parentElement?.tagName === 'P' - ? sourceLineElement.parentElement - : sourceLineElement, - ); - } - // First [data-source-line] element - // Bind all elements above it - if (i == 0) { - const highlightElements: (Element | HTMLElement)[] = []; - let siblingElement = - sourceLineElementToContainerElementMap.get(sourceLineElement) - ?.previousElementSibling; - while (siblingElement) { - if ( - !(siblingElement.tagName === 'P' && siblingElement.innerHTML === '') - ) { - highlightElements.push(siblingElement); + // First [data-source-line] element + // Bind all elements above it + if (i == 0) { + const highlightElements: (Element | HTMLElement)[] = []; + let siblingElement = + sourceLineElementToContainerElementMap.get(sourceLineElement) + ?.previousElementSibling; + while (siblingElement) { + if ( + !( + siblingElement.tagName === 'P' && + siblingElement.innerHTML === '' + ) + ) { + highlightElements.push(siblingElement); + } + siblingElement = siblingElement.previousElementSibling; } - siblingElement = siblingElement.previousElementSibling; + bindHighlightElementsEvent(highlightElements, 0); } - bindHighlightElementsEvent(highlightElements, 0); - } - // Check elements between this and the next [data-source-line] element who has the same parent - if (i < sourceLineElements.length - 1) { - for (let j = i + 1; j < sourceLineElements.length; j++) { - const nextSourceLineElement = sourceLineElements[j]; - const sourceLineElementContainer = - sourceLineElementToContainerElementMap.get(sourceLineElement); - const nextSourceLineElementContainer = - sourceLineElementToContainerElementMap.get(nextSourceLineElement); - if ( - sourceLineElementContainer && - nextSourceLineElementContainer && - sourceLineElementContainer !== nextSourceLineElementContainer && - sourceLineElementContainer.parentElement === - nextSourceLineElementContainer.parentElement - ) { - const highlightElements: (Element | HTMLElement)[] = []; - let siblingElement = sourceLineElementContainer.nextElementSibling; - while ( - siblingElement && - siblingElement !== nextSourceLineElementContainer + // Check elements between this and the next [data-source-line] element who has the same parent + if (i < sourceLineElements.length - 1) { + for (let j = i + 1; j < sourceLineElements.length; j++) { + const nextSourceLineElement = sourceLineElements[j]; + const sourceLineElementContainer = + sourceLineElementToContainerElementMap.get(sourceLineElement); + const nextSourceLineElementContainer = + sourceLineElementToContainerElementMap.get(nextSourceLineElement); + if ( + sourceLineElementContainer && + nextSourceLineElementContainer && + sourceLineElementContainer !== nextSourceLineElementContainer && + sourceLineElementContainer.parentElement === + nextSourceLineElementContainer.parentElement ) { - if ( - !( - siblingElement.tagName === 'P' && - siblingElement.innerHTML === '' - ) + const highlightElements: (Element | HTMLElement)[] = []; + let siblingElement = + sourceLineElementContainer.nextElementSibling; + while ( + siblingElement && + siblingElement !== nextSourceLineElementContainer ) { - highlightElements.push(siblingElement); + if ( + !( + siblingElement.tagName === 'P' && + siblingElement.innerHTML === '' + ) + ) { + highlightElements.push(siblingElement); + } + siblingElement = siblingElement.nextElementSibling; } - siblingElement = siblingElement.nextElementSibling; - } - bindHighlightElementsEvent(highlightElements, dataSourceLine); - break; + bindHighlightElementsEvent(highlightElements, dataSourceLine); + break; + } } } } - } - highlightElementLines.current = Array.from(startLinesSet).sort( - (a, b) => a - b, - ); - }, []); + highlightElementLines.current = Array.from(startLinesSet).sort( + (a, b) => a - b, + ); + }, + [enablePreviewZenMode], + ); const updateHtml = useCallback( (html: string, id: string, classes: string) => { @@ -1137,7 +1155,9 @@ const PreviewContainer = createContainer(() => { previewElement.current.id = id || ''; previewElement.current.setAttribute( 'class', - `crossnote markdown-preview ${classes}`, + `crossnote markdown-preview ${ + enablePreviewZenMode ? 'zen-mode' : '' + } ${classes}`, ); // scroll to initial position @@ -1166,6 +1186,7 @@ const PreviewContainer = createContainer(() => { isPresentationMode, postMessage, scrollToRevealSourceLine, + enablePreviewZenMode, ], ); @@ -1640,6 +1661,7 @@ const PreviewContainer = createContainer(() => { clickSidebarTocButton, config, contextMenuId, + enablePreviewZenMode, getHighlightElementLineRange, hiddenPreviewElement, highlightElement, diff --git a/styles/preview.less b/styles/preview.less index 92f3f735..87d724e0 100644 --- a/styles/preview.less +++ b/styles/preview.less @@ -221,6 +221,12 @@ body:not([data-presentation-mode]) { } } + .crossnote.zen-mode { + .final-line.end-of-document { + display: none; + } + } + &[data-preview-theme='dark'] { .final-line.end-of-document { color: #555; diff --git a/styles/prism_theme/github-dark.less b/styles/prism_theme/github-dark.less index 1ab2b51f..3dbdb619 100644 --- a/styles/prism_theme/github-dark.less +++ b/styles/prism_theme/github-dark.less @@ -104,7 +104,6 @@ pre[class*='language-'] > code[class*='language-'] { .language-css .token.string, .style .token.string { color: #a5d6ff; - background: #161b22; } .token.atrule, .token.attr-value,