Skip to content

Commit

Permalink
Fix to bold/italic HTML conversion with URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
jgclark committed Sep 7, 2024
1 parent 804e28d commit 173f7a5
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 121 deletions.
111 changes: 45 additions & 66 deletions helpers/HTMLView.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getStoredWindowRect, isHTMLWindowOpen, storeWindowRect } from '@helpers
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
import { isTermInNotelinkOrURI } from '@helpers/paragraph'
import { RE_EVENT_LINK, RE_SYNC_MARKER } from '@helpers/regex'
import { stringIsWithinURI } from '@helpers/stringTransforms'
import { getTimeBlockString, isTimeBlockLine } from '@helpers/timeblocks'

// ---------------------------------------------------------
Expand Down Expand Up @@ -758,40 +759,66 @@ export async function sendBannerMessage(windowId: string, message: string, color
return await sendToHTMLWindow(windowId, 'SHOW_BANNER', { warn: true, msg: message, color, border })
}

// add basic ***bolditalic*** styling
// add basic **bold** or __bold__ styling
// add basic *italic* or _italic_ styling
/**
* add basic ***bolditalic*** styling
* add basic **bold** or __bold__ styling
* add basic *italic* or _italic_ styling
* In each of these, if the text is within a URL, don't add the ***bolditalic*** or **bold** or *italic* styling
* @param {string} input
* @returns
*/
export function convertBoldAndItalicToHTML(input: string): string {
let output = input
const RE_URL = new RegExp(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, 'g')
const urls = input.match(RE_URL) ?? []
clo(urls, 'urls')

// start with ***bolditalic*** styling
const RE_BOLD_ITALIC_PHRASE = new RegExp(/\*\*\*\b(.*?)\b\*\*\*/, 'g')
let captures = output.matchAll(RE_BOLD_ITALIC_PHRASE)
if (captures) {
for (const capture of captures) {
// logDebug('convertBoldAndItalicToHTML', `- making bold-italic with [${String(capture)}]`)
output = output.replace(capture[0], `<b><em>${capture[1]}</em></b>`)
const BIMatches = output.match(RE_BOLD_ITALIC_PHRASE)
if (BIMatches) {
clo(BIMatches, 'BIMatches')
const filteredMatches = BIMatches.filter(match => {
const index = input.indexOf(match)
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
})
for (const match of filteredMatches) {
logDebug('convertBoldAndItalicToHTML', `- making bold-italic with [${String(match)}]`)
output = output.replace(match, `<b><em>${match.slice(3, match.length - 3)}</em></b>`)
}
}

// add basic **bold** or __bold__ styling
const RE_BOLD_PHRASE = new RegExp(/([_\*]{2})([^_*]+?)\1/, 'g')
captures = output.matchAll(RE_BOLD_PHRASE)
if (captures) {
for (const capture of captures) {
// logDebug('convertBoldAndItalicToHTML', `- making bold with [${String(capture)}]`)
output = output.replace(capture[0], `<b>${capture[2]}</b>`)
const boldMatches = output.match(RE_BOLD_PHRASE)
if (boldMatches) {
clo(boldMatches, 'boldMatches')
const filteredMatches = boldMatches.filter(match => {
const index = input.indexOf(match)
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
})
for (const match of filteredMatches) {
logDebug('convertBoldAndItalicToHTML', `- making bold with [${String(match)}]`)
output = output.replace(match, `<b>${match.slice(2, match.length - 2)}</b>`)
}
}

// add basic *italic* or _italic_ styling
// Note: uses a simplified regex that needs to come after bold above
const RE_ITALIC_PHRASE = new RegExp(/([_\*])([^*]+?)\1/, 'g')
captures = output.matchAll(RE_ITALIC_PHRASE)
if (captures) {
for (const capture of captures) {
// logDebug('convertBoldAndItalicToHTML', `- making italic with [${String(capture)}]`)
output = output.replace(capture[0], `<em>${capture[2]}</em>`)
const italicMatches = output.match(RE_ITALIC_PHRASE)
if (italicMatches) {
clo(italicMatches, 'italicMatches')
const filteredMatches = italicMatches.filter(match => {
const index = input.indexOf(match)
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
})
for (const match of filteredMatches) {
logDebug('convertBoldAndItalicToHTML', `- making italic with [${String(match)}]`)
output = output.replace(match, `<em>${match.slice(1, match.length - 1)}</em>`)
}
}
logDebug('convertBoldAndItalicToHTML', `-> ${output}`)
return output
}

Expand Down Expand Up @@ -956,54 +983,6 @@ export function convertNPBlockIDToHTML(input: string): string {
return output
}

/**
* Truncate visible part of HTML string, without breaking the HTML tags, or markdown links.
* @param {string} htmlIn
* @param {number} maxLength of output
* @param {boolean} dots - add ellipsis to end?
* @returns {string} truncated HTML
* TODO: write tests for this
*/
export function truncateHTML(htmlIn: string, maxLength: number, dots: boolean = true): string {
let inHTMLTag = false
let inMDLink = false
let truncatedHTML = ''
let lengthLeft = maxLength
for (let index = 0; index < htmlIn.length; index++) {
if (!lengthLeft || lengthLeft === 0) {
// no lengthLeft: stop processing
break
}
if (htmlIn[index] === '<' && htmlIn.slice(index).includes('>')) {
// if we've started an HTML tag stop counting
// logDebug('truncateHTML', `started HTML tag at ${String(index)}`)
inHTMLTag = true
}
if (htmlIn[index] === '[' && htmlIn.slice(index).match(/\]\(.*\)/)) {
// if we've started a MD link tag stop counting
// logDebug('truncateHTML', `started MD link at ${String(index)}`)
inMDLink = true
}
if (!inHTMLTag && !inMDLink) {
lengthLeft--
}
if (htmlIn[index] === '>' && inHTMLTag) {
// logDebug('truncateHTML', `stopped HTML tag at ${String(index)}`)
inHTMLTag = false
}
if (htmlIn[index] === ')' && inMDLink) {
// logDebug('truncateHTML', `stopped MD link at ${String(index)}`)
inMDLink = false
}
truncatedHTML += htmlIn[index]
}
if (dots) {
truncatedHTML = `${truncatedHTML} …`
}
// logDebug('truncateHTML', `{${htmlIn}} -> {${truncatedHTML}}`)
return truncatedHTML
}

/**
* Make HTML for a real button that is used to call a plugin's command, by sending params for a invokePluginCommandByName() call
* Note: follows earlier makeRealCallbackButton()
Expand Down
2 changes: 1 addition & 1 deletion helpers/NPCalendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import {
// printDateRange,
RE_ISO_DATE,
RE_BARE_WEEKLY_DATE,
removeDateTagsAndToday,
todaysDateISOString,
weekStartDateStr,
} from './dateTime'
import { clo, logDebug, logError, logInfo, logWarn } from './dev'
import { displayTitle } from './general'
import { findEndOfActivePartOfNote } from './paragraph'
import { removeDateTagsAndToday } from './stringTransforms'
import {
RE_TIMEBLOCK,
isTimeBlockPara,
Expand Down
63 changes: 61 additions & 2 deletions helpers/__tests__/HTMLView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import colors from 'chalk'
import * as h from '../HTMLView'
import * as n from '../NPThemeToCSS'
import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, Note, Paragraph } from '@mocks/index'
import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, /*Note, Paragraph*/ } from '@mocks/index'

beforeAll(() => {
global.Calendar = Calendar
Expand All @@ -12,7 +12,7 @@ beforeAll(() => {
global.DataStore = DataStore
global.Editor = Editor
global.NotePlan = NotePlan
DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging
DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging
})

// import { clo, logDebug, logError, logWarn } from '@helpers/dev'
Expand Down Expand Up @@ -210,3 +210,62 @@ describe('replaceMarkdownLinkWithHTMLLink()' /* function */, () => {
expect(result).toEqual(expected)
})
})

/*
* convertBoldAndItalicToHTML()
*/
describe('convertBoldAndItalicToHTML()' /* function */, () => {
test('with no url or bold/italic', () => {
const orig = 'foo bar and nothing else'
const result = h.convertBoldAndItalicToHTML(orig)
expect(result).toEqual(orig)
})
test('with url', () => {
const orig = 'Has a URL [NP Help](http://help.noteplan.co/) and nothing else'
const result = h.convertBoldAndItalicToHTML(orig)
expect(result).toEqual(orig)
})
test('with bold-italic and bold', () => {
const orig = 'foo **bar** and ***nothing else*** ok?'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <b>bar</b> and <b><em>nothing else</em></b> ok?'
expect(result).toEqual(expected)
})
test('with bold', () => {
const orig = 'foo **bar** and __nothing else__ ok?'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <b>bar</b> and <b>nothing else</b> ok?'
expect(result).toEqual(expected)
})
test('with bold and some in a URL', () => {
const orig = 'foo **bar** and http://help.noteplan.co/something/this__and__that a more complex URL'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <b>bar</b> and http://help.noteplan.co/something/this__and__that a more complex URL'
expect(result).toEqual(expected)
})
test('with bold and some in a URL', () => {
const orig = 'foo **bar** and http://help.noteplan.co/something/this__end with a later__ to ignore'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <b>bar</b> and http://help.noteplan.co/something/this__end with a later__ to ignore'
expect(result).toEqual(expected)
})

test('with italic', () => {
const orig = 'foo *bar* and _nothing else_ ok?'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <em>bar</em> and <em>nothing else</em> ok?'
expect(result).toEqual(expected)
})
test('with italic and some in a URL', () => {
const orig = 'foo *bar* and http://help.noteplan.co/something/this_and_that a more complex URL'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <em>bar</em> and http://help.noteplan.co/something/this_and_that a more complex URL'
expect(result).toEqual(expected)
})
test('with italic and some in a URL', () => {
const orig = 'foo *bar* and http://help.noteplan.co/something/this_end with a later_ to ignore'
const result = h.convertBoldAndItalicToHTML(orig)
const expected = 'foo <em>bar</em> and http://help.noteplan.co/something/this_end with a later_ to ignore'
expect(result).toEqual(expected)
})
})
24 changes: 0 additions & 24 deletions helpers/__tests__/dateTime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,30 +418,6 @@ describe(`${PLUGIN_NAME}`, () => {
})
})

describe('removeDateTagsAndToday', () => {
test('should remove ">today at end" ', () => {
expect(dt.removeDateTagsAndToday(`test >today`)).toEqual('test')
})
test('should remove ">today at beginning" ', () => {
expect(dt.removeDateTagsAndToday(`>today test`)).toEqual(' test')
})
test('should remove ">today in middle" ', () => {
expect(dt.removeDateTagsAndToday(`this is a >today test`)).toEqual('this is a test')
})
test('should remove >YYYY-MM-DD date', () => {
expect(dt.removeDateTagsAndToday(`test >2021-11-09 `)).toEqual('test')
})
test('should remove nothing if no date tag ', () => {
expect(dt.removeDateTagsAndToday(`test no date`)).toEqual('test no date')
})
test('should work for single >week also ', () => {
expect(dt.removeDateTagsAndToday(`test >2000-W02`, true)).toEqual('test')
})
test('should work for many items in a line ', () => {
expect(dt.removeDateTagsAndToday(`test >2000-W02 >2020-01-01 <2020-02-02 >2020-09-28`, true)).toEqual('test')
})
})

describe('calcOffsetDateStr', () => {
describe('should pass', () => {
test('20220101 +1d', () => {
Expand Down
85 changes: 74 additions & 11 deletions helpers/__tests__/stringTransforms.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,45 @@ const PLUGIN_NAME = `📙 ${colors.yellow('helpers/dateManipulation')}`
// const section = colors.blue

describe(`${PLUGIN_NAME}`, () => {
/*

describe('truncateHTML', () => {
test('no change as maxLength is 0', () => {
const htmlIn = '<p>This is a <strong>bold</strong> paragraph of text.</p>'
const maxLength = 0
expect(st.truncateHTML(htmlIn, maxLength)).toBe(htmlIn)
})
test('no change as maxLength is larger than htmlIn length', () => {
const htmlIn = '<p>This is a <strong>bold</strong> paragraph of text.</p>'
const maxLength = 100
expect(st.truncateHTML(htmlIn, maxLength)).toBe(htmlIn)
})
test('truncates HTML string to specified length', () => {
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
const maxLength = 20
const expectedOutput = '<p>This is a long parag…</p>'
expect(st.truncateHTML(htmlIn, maxLength)).toBe(expectedOutput)
})
test('preserves markdown links', () => {
const htmlIn = '<p>This is a [link](http://example.com) to a website.</p>'
const maxLength = 15
const expectedOutput = '<p>This is a [link](http://example.com) to a…</p>'
expect(st.truncateHTML(htmlIn, maxLength)).toBe(expectedOutput)
})
test('adds ellipsis if dots is true', () => {
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
const maxLength = 20
const expectedOutput = '<p>This is a long parag…</p>'
expect(st.truncateHTML(htmlIn, maxLength, true)).toBe(expectedOutput)
})

test('does not add ellipsis if dots is false', () => {
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
const maxLength = 20
const expectedOutput = '<p>This is a long parag</p>'
expect(st.truncateHTML(htmlIn, maxLength, false)).toBe(expectedOutput)
})
})
/*
* changeMarkdownLinksToHTMLLink()
*/
describe('changeMarkdownLinksToHTMLLink()' /* function */, () => {
Expand Down Expand Up @@ -73,31 +111,31 @@ describe(`${PLUGIN_NAME}`, () => {
})
test('should produce HTML link 1 with icon and no truncation', () => {
const input = 'this has a https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long valid bare link'
const result = st.changeBareLinksToHTMLLink(input, true, false)
const result = st.changeBareLinksToHTMLLink(input, true)
expect(result).toEqual(
'this has a <a class="externalLink" href="https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long"><i class="fa-regular fa-globe pad-right"></i>https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long</a> valid bare link')
})
test('should produce HTML link 1 with icon and truncation', () => {
const input = 'this has a https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long valid bare link'
const result = st.changeBareLinksToHTMLLink(input, true, true)
const result = st.changeBareLinksToHTMLLink(input, true, 21)
expect(result).toEqual(
'this has a <a class="externalLink" href="https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long"><i class="fa-regular fa-globe pad-right"></i>https://www.something.com/with?various&chars%20ok/...</a> valid bare link')
'this has a <a class="externalLink" href="https://www.something.com/with?various&chars%20ok/~/and/yet/more/things-to-make-it-really-quite-long"><i class="fa-regular fa-globe pad-right"></i>https://www.something</a> valid bare link')
})
test('should produce HTML link 1 without icon', () => {
const input = 'this has a https://www.something.com/with?various&chars%20ok valid bare link'
const result = st.changeBareLinksToHTMLLink(input, false, false)
const result = st.changeBareLinksToHTMLLink(input, false)
expect(result).toEqual(
'this has a <a class="externalLink" href="https://www.something.com/with?various&chars%20ok">https://www.something.com/with?various&chars%20ok</a> valid bare link')
})
test('should produce HTML link when a link takes up the whole line', () => {
test('should produce HTML link when a link takes up the whole line with icon', () => {
const input = 'https://www.something.com/with?various&chars%20ok'
const result = st.changeBareLinksToHTMLLink(input, true, false)
const result = st.changeBareLinksToHTMLLink(input, true)
expect(result).toEqual('<a class="externalLink" href="https://www.something.com/with?various&chars%20ok"><i class="fa-regular fa-globe pad-right"></i>https://www.something.com/with?various&chars%20ok</a>')
})
test('should produce HTML link when a link takes up the whole line', () => {
const input = 'https://www.something.com/with?various&chars%20ok'
const result = st.changeBareLinksToHTMLLink(input, true, false)
expect(result).toEqual('<a class="externalLink" href="https://www.something.com/with?various&chars%20ok"><i class="fa-regular fa-globe pad-right"></i>https://www.something.com/with?various&chars%20ok</a>')
test('should produce truncated HTML link with a very long bare link', () => {
const input = 'https://validation.poweredbypercent.com/validate/validationinvite_eb574173-f781-4946-b0be-9a06f838289e?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyUHVibGljS2V5IjoicGtfM2YzNzFmMmYtYjQ3MC00M2Q1LTk2MDUtZGMxYTU4YjhjY2IzIiwiaWF0IjoxNzI1NjA5MTkyfQ.GM5ITBbgUHd5Qsyq-d_lkOFIqmTuYJH4Kc4DNIoibE0'
const result = st.changeBareLinksToHTMLLink(input, false, 50)
expect(result).toEqual('<a class="externalLink" href="https://validation.poweredbypercent.com/validate/validationinvite_eb574173-f781-4946-b0be-9a06f838289e?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyUHVibGljS2V5IjoicGtfM2YzNzFmMmYtYjQ3MC00M2Q1LTk2MDUtZGMxYTU4YjhjY2IzIiwiaWF0IjoxNzI1NjA5MTkyfQ.GM5ITBbgUHd5Qsyq-d_lkOFIqmTuYJH4Kc4DNIoibE0">https://validation.poweredbypercent.com/validate/v…</a>')
})
})

Expand Down Expand Up @@ -432,4 +470,29 @@ describe(`${PLUGIN_NAME}`, () => {
})
})
})

describe('removeDateTagsAndToday', () => {
test('should remove ">today at end" ', () => {
expect(st.removeDateTagsAndToday(`test >today`)).toEqual('test')
})
test('should remove ">today at beginning" ', () => {
expect(st.removeDateTagsAndToday(`>today test`)).toEqual(' test')
})
test('should remove ">today in middle" ', () => {
expect(st.removeDateTagsAndToday(`this is a >today test`)).toEqual('this is a test')
})
test('should remove >YYYY-MM-DD date', () => {
expect(st.removeDateTagsAndToday(`test >2021-11-09 `)).toEqual('test')
})
test('should remove nothing if no date tag ', () => {
expect(st.removeDateTagsAndToday(`test no date`)).toEqual('test no date')
})
test('should work for single >week also ', () => {
expect(st.removeDateTagsAndToday(`test >2000-W02`, true)).toEqual('test')
})
test('should work for many items in a line ', () => {
expect(st.removeDateTagsAndToday(`test >2000-W02 >2020-01-01 <2020-02-02 >2020-09-28`, true)).toEqual('test')
})
})

})
Loading

0 comments on commit 173f7a5

Please sign in to comment.