Skip to content

Commit 470a12e

Browse files
authored
Merge pull request #624 from cwhittl/cw-move-content-to-html-to-share-and-fix-images
Moved Content to HTML functionallity to Shared API and implement IMG files.
2 parents e99cf4c + ddc0cdd commit 470a12e

File tree

3 files changed

+164
-129
lines changed

3 files changed

+164
-129
lines changed

helpers/HTMLView.js

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
// by @jgclark, @dwertheimer
55
// Last updated 2024-11-09 by @jgclark
66
// ---------------------------------------------------------
7-
7+
import showdown from 'showdown' // for Markdown -> HTML from https://github.com/showdownjs/showdown
8+
import {
9+
hasFrontMatter
10+
} from '@helpers/NPFrontMatter'
11+
import { getFolderFromFilename } from '@helpers/folders'
812
import { clo, logDebug, logError, logInfo, logWarn, JSP, timer } from '@helpers/dev'
913
import { getStoredWindowRect, isHTMLWindowOpen, storeWindowRect } from '@helpers/NPWindows'
1014
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
1115
import { isTermInNotelinkOrURI } from '@helpers/paragraph'
12-
import { RE_EVENT_LINK, RE_SYNC_MARKER } from '@helpers/regex'
16+
import { RE_EVENT_LINK, RE_SYNC_MARKER, formRegExForUsersOpenTasks } from '@helpers/regex'
1317
import { getTimeBlockString, isTimeBlockLine } from '@helpers/timeblocks'
1418

1519
// ---------------------------------------------------------
@@ -91,6 +95,147 @@ export function getCallbackCodeString(jsFunctionName: string, commandName: strin
9195
`
9296
}
9397

98+
99+
/**
100+
* Convert a note's content to HTML and include any images as base64
101+
* @param {string} content
102+
* @param {TNote} Note
103+
* @returns {string} HTML
104+
*/
105+
export async function getNoteContentAsHTML(content: string, note: TNote): ?string {
106+
try {
107+
let lines = content?.split('\n') ?? []
108+
109+
let hasFrontmatter = hasFrontMatter(content ?? '')
110+
const RE_OPEN_TASK_FOR_USER = formRegExForUsersOpenTasks(false)
111+
112+
// Work on a copy of the note's content
113+
// Change frontmatter for this note (if present)
114+
// In particular remove trigger line
115+
if (hasFrontmatter) {
116+
let titleAsMD = ''
117+
// look for 2nd '---' and double it, because of showdown bug
118+
for (let i = 1; i < lines.length; i++) {
119+
if (lines[i].match(/^title:\s/)) {
120+
titleAsMD = lines[i].replace('title:', '#')
121+
logDebug('previewNote', `removing title line ${String(i)}`)
122+
lines.splice(i, 1)
123+
}
124+
if (lines[i].trim() === '---') {
125+
lines.splice(i, 0, '') // add a blank before second HR to stop it acting as an ATX header line
126+
lines.splice(i + 2, 0, titleAsMD) // add the title (as MD)
127+
break
128+
}
129+
}
130+
131+
// If we now have empty frontmatter (so, just 3 sets of '---'), then remove them all
132+
if (lines[0] === '---' && lines[1] === '' && lines[2] === '---') {
133+
lines.splice(0, 3)
134+
hasFrontmatter = false
135+
}
136+
}
137+
138+
// Make some necessary changes before conversion to HTML
139+
for (let i = 0; i < lines.length; i++) {
140+
// remove any sync link markers (blockIds)
141+
lines[i] = lines[i].replace(/\^[A-z0-9]{6}([^A-z0-9]|$)/g, '').trimRight()
142+
143+
// change open tasks to GFM-flavoured task syntax
144+
const res = lines[i].match(RE_OPEN_TASK_FOR_USER)
145+
if (res) {
146+
lines[i] = lines[i].replace(res[0], '- [ ]')
147+
}
148+
}
149+
150+
// Make this proper Markdown -> HTML via showdown library
151+
// Set some options to turn on various more advanced HTML conversions (see actual code at https://github.com/showdownjs/showdown/blob/master/src/options.js#L109):
152+
const converterOptions = {
153+
emoji: true,
154+
footnotes: true,
155+
ghCodeBlocks: true,
156+
strikethrough: true,
157+
tables: true,
158+
tasklists: true,
159+
metadata: false, // otherwise metadata is swallowed
160+
requireSpaceBeforeHeadingText: true,
161+
simpleLineBreaks: true // Makes this GFM style. TODO: make an option?
162+
}
163+
const converter = new showdown.Converter(converterOptions)
164+
let body = converter.makeHtml(lines.join(`\n`))
165+
body = `<style>img { max-width: 100%; max-height: 100%; }</style>${body}` // fix for bug in showdown
166+
167+
const imgTagRegex = /<img src=\"(.*?)\"/g
168+
const matches = [...body.matchAll(imgTagRegex)]
169+
const noteDirPath = getFolderFromFilename(note.filename)
170+
171+
for (const match of matches) {
172+
const imagePath = match[1]
173+
try {
174+
// Handle both absolute and relative paths
175+
const fullPath = `../../../Notes/${noteDirPath}/${decodeURI(imagePath)}`
176+
177+
const data = await DataStore.loadData(fullPath)
178+
if (data) {
179+
const base64Data = `data:image/png;base64,${data.toString('base64')}`
180+
body = body.replaceAll(imagePath, base64Data)
181+
}
182+
} catch (err) {
183+
logWarn("Failed to load image", imagePath, err)
184+
}
185+
}
186+
187+
// TODO: Ideally build a frontmatter styler extension (to use above) but for now ...
188+
// Tweak body output to put frontmatter in a box if it exists
189+
if (hasFrontmatter) {
190+
// replace first '<hr />' with start of div
191+
body = body.replace('<hr />', '<div class="frontmatter">')
192+
// replace what is now the first '<hr />' with end of div
193+
body = body.replace('<hr />', '</div>')
194+
}
195+
// logDebug(pluginJson, body)
196+
197+
// Make other changes to the HTML to cater for NotePlan-specific syntax
198+
lines = body.split('\n')
199+
const modifiedLines = []
200+
for (let line of lines) {
201+
const origLine = line
202+
203+
// Display hashtags with .hashtag style
204+
line = convertHashtagsToHTML(line)
205+
206+
// Display mentions with .attag style
207+
line = convertMentionsToHTML(line)
208+
209+
// Display highlights with .highlight style
210+
line = convertHighlightsToHTML(line)
211+
212+
// Replace [[notelinks]] with just underlined notelink
213+
const captures = line.match(/\[\[(.*?)\]\]/)
214+
if (captures) {
215+
// clo(captures, 'results from [[notelinks]] match:')
216+
for (const capturedTitle of captures) {
217+
line = line.replace(`[[${capturedTitle}]]`, `~${capturedTitle}~`)
218+
}
219+
}
220+
// Display underlining with .underlined style
221+
line = convertUnderlinedToHTML(line)
222+
223+
// Remove any blockIDs
224+
line = line.replace(RE_SYNC_MARKER, '')
225+
226+
if (line !== origLine) {
227+
logDebug('previewNote', `modified {${origLine}} -> {${line}}`)
228+
}
229+
modifiedLines.push(line)
230+
}
231+
return modifiedLines.join('\n')
232+
233+
} catch (error) {
234+
logError('Converting To HTML', error.message)
235+
}
236+
}
237+
238+
94239
/**
95240
* This function creates the webkit console.log/error handler for HTML messages to get back to NP console.log
96241
* @returns {string} - the javascript (without a tag)

helpers/note.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { getFolderListMinusExclusions, getFolderFromFilename } from '@helpers/fo
2424
import { displayTitle, type headingLevelType } from '@helpers/general'
2525
import { toNPLocaleDateString } from '@helpers/NPdateTime'
2626
import { findEndOfActivePartOfNote, findStartOfActivePartOfNote } from '@helpers/paragraph'
27-
import { formRegExForUsersOpenTasks } from '@helpers/regex'
2827
import { sortListBy } from '@helpers/sorting'
2928
import {
3029
isOpen,

np.Preview/src/previewMain.js

Lines changed: 17 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,18 @@
55
// by Jonathan Clark, last updated 11.8.2023 for v0.4.?
66
//--------------------------------------------------------------
77

8-
import pluginJson from '../plugin.json'
8+
99
// import open, { openApp, apps } from 'open'
10-
import showdown from 'showdown' // for Markdown -> HTML from https://github.com/showdownjs/showdown
11-
import { getCodeBlocksOfType } from '@helpers/codeBlocks'
12-
import { clo, JSP, logDebug, logError, logInfo, logWarn } from '@helpers/dev'
10+
import pluginJson from '../plugin.json'
11+
import { logDebug, logError, logWarn } from '@helpers/dev'
1312
import { addTrigger } from '@helpers/NPFrontMatter'
1413
import { displayTitle } from '@helpers/general'
14+
1515
import {
16-
getFrontMatterParagraphs,
17-
hasFrontMatter
18-
} from '@helpers/NPFrontMatter'
19-
import {
20-
convertHashtagsToHTML,
21-
convertHighlightsToHTML,
22-
convertMentionsToHTML,
23-
convertUnderlinedToHTML,
16+
getNoteContentAsHTML,
2417
type HtmlWindowOptions,
2518
showHTMLV2
2619
} from '@helpers/HTMLView'
27-
import { formRegExForUsersOpenTasks, RE_SYNC_MARKER } from '@helpers/regex'
2820
import { showMessageYesNo } from '@helpers/userInput'
2921

3022
//--------------------------------------------------------------
@@ -82,41 +74,11 @@ Button a { text-decoration: none; font-size: 0.9rem; }
8274
* @author @jgclark
8375
* @param {string?} mermaidTheme name (optional)
8476
*/
85-
export function previewNote(mermaidTheme?: string): void {
77+
export async function previewNote(mermaidTheme?: string = "green"): void {
8678
try {
87-
const { note, content, title } = Editor
79+
const { note, content } = Editor
8880
let lines = content?.split('\n') ?? []
89-
let hasFrontmatter = hasFrontMatter(content ?? '')
90-
const RE_OPEN_TASK_FOR_USER = formRegExForUsersOpenTasks(false)
91-
92-
// Work on a copy of the note's content
93-
// Change frontmatter for this note (if present)
94-
// In particular remove trigger line
95-
if (hasFrontmatter) {
96-
let titleAsMD = ''
97-
lines = lines.filter(l => l !== 'triggers: onEditorWillSave => np.Preview.updatePreview')
98-
// look for 2nd '---' and double it, because of showdown bug
99-
for (let i = 1; i < lines.length; i++) {
100-
if (lines[i].match(/^title:\s/)) {
101-
titleAsMD = lines[i].replace('title:', '#')
102-
logDebug('previewNote', `removing title line ${String(i)}`)
103-
lines.splice(i, 1)
104-
}
105-
if (lines[i].trim() === '---') {
106-
lines.splice(i, 0, '') // add a blank before second HR to stop it acting as an ATX header line
107-
lines.splice(i + 2, 0, titleAsMD) // add the title (as MD)
108-
break
109-
}
110-
}
111-
112-
// If we now have empty frontmatter (so, just 3 sets of '---'), then remove them all
113-
if (lines[0] === '---' && lines[1] === '' && lines[2] === '---') {
114-
lines.splice(0, 3)
115-
hasFrontmatter = false
116-
}
117-
}
118-
119-
81+
lines = lines.filter(l => l !== 'triggers: onEditorWillSave => np.Preview.updatePreview')
12082
// Update mermaid fenced code blocks to suitable <divs>
12183
// Note: did try to use getCodeBlocksOfType() helper but found it wasn't architected helpfully for this use case
12284
let includesMermaid = false
@@ -133,89 +95,18 @@ export function previewNote(mermaidTheme?: string): void {
13395
}
13496
}
13597

136-
// Make some necessary changes before conversion to HTML
137-
for (let i = 0; i < lines.length; i++) {
138-
// remove any sync link markers (blockIds)
139-
lines[i] = lines[i].replace(/\^[A-z0-9]{6}([^A-z0-9]|$)/g, '').trimRight()
140-
141-
// change open tasks to GFM-flavoured task syntax
142-
const res = lines[i].match(RE_OPEN_TASK_FOR_USER)
143-
if (res) {
144-
lines[i] = lines[i].replace(res[0], '- [ ]')
145-
}
146-
}
98+
let body = await getNoteContentAsHTML(lines.join('\n'), note)
14799

148-
// Make this proper Markdown -> HTML via showdown library
149-
// Set some options to turn on various more advanced HTML conversions (see actual code at https://github.com/showdownjs/showdown/blob/master/src/options.js#L109):
150-
const converterOptions = {
151-
emoji: true,
152-
footnotes: true,
153-
ghCodeBlocks: true,
154-
strikethrough: true,
155-
tables: true,
156-
tasklists: true,
157-
metadata: false, // otherwise metadata is swallowed
158-
requireSpaceBeforeHeadingText: true,
159-
simpleLineBreaks: true // Makes this GFM style. TODO: make an option?
160-
}
161-
const converter = new showdown.Converter(converterOptions)
162-
let body = converter.makeHtml(lines.join(`\n`))
163-
164-
// logDebug(pluginJson, 'Converter produces:\n' + body)
165-
166-
// TODO: Ideally build a frontmatter styler extension (to use above) but for now ...
167-
// Tweak body output to put frontmatter in a box if it exists
168-
if (hasFrontmatter) {
169-
// replace first '<hr />' with start of div
170-
body = body.replace('<hr />', '<div class="frontmatter">')
171-
// replace what is now the first '<hr />' with end of div
172-
body = body.replace('<hr />', '</div>')
173-
}
174-
// logDebug(pluginJson, body)
175-
176-
// Make other changes to the HTML to cater for NotePlan-specific syntax
177-
lines = body.split('\n')
178-
let modifiedLines = []
179-
for (let line of lines) {
180-
const origLine = line
181-
182-
// Display hashtags with .hashtag style
183-
line = convertHashtagsToHTML(line)
184-
185-
// Display mentions with .attag style
186-
line = convertMentionsToHTML(line)
187-
188-
// Display highlights with .highlight style
189-
line = convertHighlightsToHTML(line)
190-
191-
// Replace [[notelinks]] with just underlined notelink
192-
let captures = line.match(/\[\[(.*?)\]\]/)
193-
if (captures) {
194-
// clo(captures, 'results from [[notelinks]] match:')
195-
for (let capturedTitle of captures) {
196-
line = line.replace('[[' + capturedTitle + ']]', '~' + capturedTitle + '~')
197-
}
198-
}
199-
// Display underlining with .underlined style
200-
line = convertUnderlinedToHTML(line)
201-
202-
// Remove any blockIDs
203-
line = line.replace(RE_SYNC_MARKER, '')
204-
205-
if (line !== origLine) {
206-
logDebug('previewNote', `modified {${origLine}} -> {${line}}`)
207-
}
208-
modifiedLines.push(line)
209-
}
210100
// Add mermaid script if needed
211-
const finalBody = modifiedLines.join('\n') + (includesMermaid ? initMermaidScripts(mermaidTheme) : '')
212-
console.log(initMermaidScripts("green"))
213-
101+
if (includesMermaid) {
102+
body = initMermaidScripts(mermaidTheme) + body
103+
}
214104
// Add sticky button at top right offering to print
215105
// (But printing doesn't work on i(Pad)OS ...)
216106
if (NotePlan.environment.platform === 'macOS') {
217-
body = ` <div class="stickyButton"><button class="nonPrinting" type="printButton"><a href="preview.html" onclick="window.open(this.href).print(); return false;">Print (opens in system browser)</a></button></div>\n` + body // Note: seems to need the .print() even though it doesn't activate in the browser.
107+
body = `<div class="stickyButton"><button class="nonPrinting" type="printButton"><a href="preview.html" onclick="window.open(this.href).print(); return false;">Print (opens in system browser)</a></button></div>\n${body}` // Note: seems to need the .print() even though it doesn't activate in the browser.
218108
}
109+
219110
const headerTags = `<meta name="generator" content="np.Preview plugin by @jgclark v${pluginJson['plugin.version'] ?? '?'}">
220111
<meta name="date" content="${new Date().toISOString()}">`
221112

@@ -234,7 +125,7 @@ export function previewNote(mermaidTheme?: string): void {
234125
shouldFocus: false, // shouuld not focus, if Window already exists
235126
// not setting defaults for x, y, width, height
236127
}
237-
showHTMLV2(finalBody, windowOpts)
128+
showHTMLV2(body, windowOpts)
238129
// logDebug('preview', `written results to HTML`)
239130
}
240131
catch (error) {
@@ -249,7 +140,7 @@ export function previewNote(mermaidTheme?: string): void {
249140
export async function openPreviewNoteInBrowser(): Promise<void> {
250141
try {
251142
// Call preview note function with 'default' theme (best for printing)
252-
previewNote('default')
143+
await previewNote('default')
253144
logDebug(pluginJson, `openPreviewNoteInBrowser: preview created; now will try to open in browser`)
254145
// FIXME: the following doesn't work -- something to do with imports and builtins
255146
// await open(savedFilename)
@@ -278,7 +169,7 @@ export async function addTriggerAndStartPreview(): Promise<void> {
278169
}
279170

280171
// Start the preview
281-
previewNote()
172+
await previewNote()
282173
}
283174
catch (error) {
284175
logError(pluginJson, `${error.name}: ${error.message}`)

0 commit comments

Comments
 (0)