Skip to content

Commit 173f7a5

Browse files
committed
Fix to bold/italic HTML conversion with URLs
1 parent 804e28d commit 173f7a5

9 files changed

Lines changed: 318 additions & 121 deletions

File tree

helpers/HTMLView.js

Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getStoredWindowRect, isHTMLWindowOpen, storeWindowRect } from '@helpers
1010
import { generateCSSFromTheme, RGBColourConvert } from '@helpers/NPThemeToCSS'
1111
import { isTermInNotelinkOrURI } from '@helpers/paragraph'
1212
import { RE_EVENT_LINK, RE_SYNC_MARKER } from '@helpers/regex'
13+
import { stringIsWithinURI } from '@helpers/stringTransforms'
1314
import { getTimeBlockString, isTimeBlockLine } from '@helpers/timeblocks'
1415

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

761-
// add basic ***bolditalic*** styling
762-
// add basic **bold** or __bold__ styling
763-
// add basic *italic* or _italic_ styling
762+
/**
763+
* add basic ***bolditalic*** styling
764+
* add basic **bold** or __bold__ styling
765+
* add basic *italic* or _italic_ styling
766+
* In each of these, if the text is within a URL, don't add the ***bolditalic*** or **bold** or *italic* styling
767+
* @param {string} input
768+
* @returns
769+
*/
764770
export function convertBoldAndItalicToHTML(input: string): string {
765771
let output = input
772+
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')
773+
const urls = input.match(RE_URL) ?? []
774+
clo(urls, 'urls')
775+
776+
// start with ***bolditalic*** styling
766777
const RE_BOLD_ITALIC_PHRASE = new RegExp(/\*\*\*\b(.*?)\b\*\*\*/, 'g')
767-
let captures = output.matchAll(RE_BOLD_ITALIC_PHRASE)
768-
if (captures) {
769-
for (const capture of captures) {
770-
// logDebug('convertBoldAndItalicToHTML', `- making bold-italic with [${String(capture)}]`)
771-
output = output.replace(capture[0], `<b><em>${capture[1]}</em></b>`)
778+
const BIMatches = output.match(RE_BOLD_ITALIC_PHRASE)
779+
if (BIMatches) {
780+
clo(BIMatches, 'BIMatches')
781+
const filteredMatches = BIMatches.filter(match => {
782+
const index = input.indexOf(match)
783+
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
784+
})
785+
for (const match of filteredMatches) {
786+
logDebug('convertBoldAndItalicToHTML', `- making bold-italic with [${String(match)}]`)
787+
output = output.replace(match, `<b><em>${match.slice(3, match.length - 3)}</em></b>`)
772788
}
773789
}
774790

775791
// add basic **bold** or __bold__ styling
776792
const RE_BOLD_PHRASE = new RegExp(/([_\*]{2})([^_*]+?)\1/, 'g')
777-
captures = output.matchAll(RE_BOLD_PHRASE)
778-
if (captures) {
779-
for (const capture of captures) {
780-
// logDebug('convertBoldAndItalicToHTML', `- making bold with [${String(capture)}]`)
781-
output = output.replace(capture[0], `<b>${capture[2]}</b>`)
793+
const boldMatches = output.match(RE_BOLD_PHRASE)
794+
if (boldMatches) {
795+
clo(boldMatches, 'boldMatches')
796+
const filteredMatches = boldMatches.filter(match => {
797+
const index = input.indexOf(match)
798+
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
799+
})
800+
for (const match of filteredMatches) {
801+
logDebug('convertBoldAndItalicToHTML', `- making bold with [${String(match)}]`)
802+
output = output.replace(match, `<b>${match.slice(2, match.length - 2)}</b>`)
782803
}
783804
}
784805

785806
// add basic *italic* or _italic_ styling
786807
// Note: uses a simplified regex that needs to come after bold above
787808
const RE_ITALIC_PHRASE = new RegExp(/([_\*])([^*]+?)\1/, 'g')
788-
captures = output.matchAll(RE_ITALIC_PHRASE)
789-
if (captures) {
790-
for (const capture of captures) {
791-
// logDebug('convertBoldAndItalicToHTML', `- making italic with [${String(capture)}]`)
792-
output = output.replace(capture[0], `<em>${capture[2]}</em>`)
809+
const italicMatches = output.match(RE_ITALIC_PHRASE)
810+
if (italicMatches) {
811+
clo(italicMatches, 'italicMatches')
812+
const filteredMatches = italicMatches.filter(match => {
813+
const index = input.indexOf(match)
814+
return !urls.some(url => input.indexOf(url) < index && input.indexOf(url) + url.length > index)
815+
})
816+
for (const match of filteredMatches) {
817+
logDebug('convertBoldAndItalicToHTML', `- making italic with [${String(match)}]`)
818+
output = output.replace(match, `<em>${match.slice(1, match.length - 1)}</em>`)
793819
}
794820
}
821+
logDebug('convertBoldAndItalicToHTML', `-> ${output}`)
795822
return output
796823
}
797824

@@ -956,54 +983,6 @@ export function convertNPBlockIDToHTML(input: string): string {
956983
return output
957984
}
958985

959-
/**
960-
* Truncate visible part of HTML string, without breaking the HTML tags, or markdown links.
961-
* @param {string} htmlIn
962-
* @param {number} maxLength of output
963-
* @param {boolean} dots - add ellipsis to end?
964-
* @returns {string} truncated HTML
965-
* TODO: write tests for this
966-
*/
967-
export function truncateHTML(htmlIn: string, maxLength: number, dots: boolean = true): string {
968-
let inHTMLTag = false
969-
let inMDLink = false
970-
let truncatedHTML = ''
971-
let lengthLeft = maxLength
972-
for (let index = 0; index < htmlIn.length; index++) {
973-
if (!lengthLeft || lengthLeft === 0) {
974-
// no lengthLeft: stop processing
975-
break
976-
}
977-
if (htmlIn[index] === '<' && htmlIn.slice(index).includes('>')) {
978-
// if we've started an HTML tag stop counting
979-
// logDebug('truncateHTML', `started HTML tag at ${String(index)}`)
980-
inHTMLTag = true
981-
}
982-
if (htmlIn[index] === '[' && htmlIn.slice(index).match(/\]\(.*\)/)) {
983-
// if we've started a MD link tag stop counting
984-
// logDebug('truncateHTML', `started MD link at ${String(index)}`)
985-
inMDLink = true
986-
}
987-
if (!inHTMLTag && !inMDLink) {
988-
lengthLeft--
989-
}
990-
if (htmlIn[index] === '>' && inHTMLTag) {
991-
// logDebug('truncateHTML', `stopped HTML tag at ${String(index)}`)
992-
inHTMLTag = false
993-
}
994-
if (htmlIn[index] === ')' && inMDLink) {
995-
// logDebug('truncateHTML', `stopped MD link at ${String(index)}`)
996-
inMDLink = false
997-
}
998-
truncatedHTML += htmlIn[index]
999-
}
1000-
if (dots) {
1001-
truncatedHTML = `${truncatedHTML} …`
1002-
}
1003-
// logDebug('truncateHTML', `{${htmlIn}} -> {${truncatedHTML}}`)
1004-
return truncatedHTML
1005-
}
1006-
1007986
/**
1008987
* Make HTML for a real button that is used to call a plugin's command, by sending params for a invokePluginCommandByName() call
1009988
* Note: follows earlier makeRealCallbackButton()

helpers/NPCalendar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ import {
2323
// printDateRange,
2424
RE_ISO_DATE,
2525
RE_BARE_WEEKLY_DATE,
26-
removeDateTagsAndToday,
2726
todaysDateISOString,
2827
weekStartDateStr,
2928
} from './dateTime'
3029
import { clo, logDebug, logError, logInfo, logWarn } from './dev'
3130
import { displayTitle } from './general'
3231
import { findEndOfActivePartOfNote } from './paragraph'
32+
import { removeDateTagsAndToday } from './stringTransforms'
3333
import {
3434
RE_TIMEBLOCK,
3535
isTimeBlockPara,

helpers/__tests__/HTMLView.test.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import colors from 'chalk'
44
import * as h from '../HTMLView'
55
import * as n from '../NPThemeToCSS'
6-
import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, Note, Paragraph } from '@mocks/index'
6+
import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, /*Note, Paragraph*/ } from '@mocks/index'
77

88
beforeAll(() => {
99
global.Calendar = Calendar
@@ -12,7 +12,7 @@ beforeAll(() => {
1212
global.DataStore = DataStore
1313
global.Editor = Editor
1414
global.NotePlan = NotePlan
15-
DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging
15+
DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging
1616
})
1717

1818
// import { clo, logDebug, logError, logWarn } from '@helpers/dev'
@@ -210,3 +210,62 @@ describe('replaceMarkdownLinkWithHTMLLink()' /* function */, () => {
210210
expect(result).toEqual(expected)
211211
})
212212
})
213+
214+
/*
215+
* convertBoldAndItalicToHTML()
216+
*/
217+
describe('convertBoldAndItalicToHTML()' /* function */, () => {
218+
test('with no url or bold/italic', () => {
219+
const orig = 'foo bar and nothing else'
220+
const result = h.convertBoldAndItalicToHTML(orig)
221+
expect(result).toEqual(orig)
222+
})
223+
test('with url', () => {
224+
const orig = 'Has a URL [NP Help](http://help.noteplan.co/) and nothing else'
225+
const result = h.convertBoldAndItalicToHTML(orig)
226+
expect(result).toEqual(orig)
227+
})
228+
test('with bold-italic and bold', () => {
229+
const orig = 'foo **bar** and ***nothing else*** ok?'
230+
const result = h.convertBoldAndItalicToHTML(orig)
231+
const expected = 'foo <b>bar</b> and <b><em>nothing else</em></b> ok?'
232+
expect(result).toEqual(expected)
233+
})
234+
test('with bold', () => {
235+
const orig = 'foo **bar** and __nothing else__ ok?'
236+
const result = h.convertBoldAndItalicToHTML(orig)
237+
const expected = 'foo <b>bar</b> and <b>nothing else</b> ok?'
238+
expect(result).toEqual(expected)
239+
})
240+
test('with bold and some in a URL', () => {
241+
const orig = 'foo **bar** and http://help.noteplan.co/something/this__and__that a more complex URL'
242+
const result = h.convertBoldAndItalicToHTML(orig)
243+
const expected = 'foo <b>bar</b> and http://help.noteplan.co/something/this__and__that a more complex URL'
244+
expect(result).toEqual(expected)
245+
})
246+
test('with bold and some in a URL', () => {
247+
const orig = 'foo **bar** and http://help.noteplan.co/something/this__end with a later__ to ignore'
248+
const result = h.convertBoldAndItalicToHTML(orig)
249+
const expected = 'foo <b>bar</b> and http://help.noteplan.co/something/this__end with a later__ to ignore'
250+
expect(result).toEqual(expected)
251+
})
252+
253+
test('with italic', () => {
254+
const orig = 'foo *bar* and _nothing else_ ok?'
255+
const result = h.convertBoldAndItalicToHTML(orig)
256+
const expected = 'foo <em>bar</em> and <em>nothing else</em> ok?'
257+
expect(result).toEqual(expected)
258+
})
259+
test('with italic and some in a URL', () => {
260+
const orig = 'foo *bar* and http://help.noteplan.co/something/this_and_that a more complex URL'
261+
const result = h.convertBoldAndItalicToHTML(orig)
262+
const expected = 'foo <em>bar</em> and http://help.noteplan.co/something/this_and_that a more complex URL'
263+
expect(result).toEqual(expected)
264+
})
265+
test('with italic and some in a URL', () => {
266+
const orig = 'foo *bar* and http://help.noteplan.co/something/this_end with a later_ to ignore'
267+
const result = h.convertBoldAndItalicToHTML(orig)
268+
const expected = 'foo <em>bar</em> and http://help.noteplan.co/something/this_end with a later_ to ignore'
269+
expect(result).toEqual(expected)
270+
})
271+
})

helpers/__tests__/dateTime.test.js

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -418,30 +418,6 @@ describe(`${PLUGIN_NAME}`, () => {
418418
})
419419
})
420420

421-
describe('removeDateTagsAndToday', () => {
422-
test('should remove ">today at end" ', () => {
423-
expect(dt.removeDateTagsAndToday(`test >today`)).toEqual('test')
424-
})
425-
test('should remove ">today at beginning" ', () => {
426-
expect(dt.removeDateTagsAndToday(`>today test`)).toEqual(' test')
427-
})
428-
test('should remove ">today in middle" ', () => {
429-
expect(dt.removeDateTagsAndToday(`this is a >today test`)).toEqual('this is a test')
430-
})
431-
test('should remove >YYYY-MM-DD date', () => {
432-
expect(dt.removeDateTagsAndToday(`test >2021-11-09 `)).toEqual('test')
433-
})
434-
test('should remove nothing if no date tag ', () => {
435-
expect(dt.removeDateTagsAndToday(`test no date`)).toEqual('test no date')
436-
})
437-
test('should work for single >week also ', () => {
438-
expect(dt.removeDateTagsAndToday(`test >2000-W02`, true)).toEqual('test')
439-
})
440-
test('should work for many items in a line ', () => {
441-
expect(dt.removeDateTagsAndToday(`test >2000-W02 >2020-01-01 <2020-02-02 >2020-09-28`, true)).toEqual('test')
442-
})
443-
})
444-
445421
describe('calcOffsetDateStr', () => {
446422
describe('should pass', () => {
447423
test('20220101 +1d', () => {

helpers/__tests__/stringTransforms.test.js

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,45 @@ const PLUGIN_NAME = `📙 ${colors.yellow('helpers/dateManipulation')}`
2323
// const section = colors.blue
2424

2525
describe(`${PLUGIN_NAME}`, () => {
26-
/*
26+
27+
describe('truncateHTML', () => {
28+
test('no change as maxLength is 0', () => {
29+
const htmlIn = '<p>This is a <strong>bold</strong> paragraph of text.</p>'
30+
const maxLength = 0
31+
expect(st.truncateHTML(htmlIn, maxLength)).toBe(htmlIn)
32+
})
33+
test('no change as maxLength is larger than htmlIn length', () => {
34+
const htmlIn = '<p>This is a <strong>bold</strong> paragraph of text.</p>'
35+
const maxLength = 100
36+
expect(st.truncateHTML(htmlIn, maxLength)).toBe(htmlIn)
37+
})
38+
test('truncates HTML string to specified length', () => {
39+
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
40+
const maxLength = 20
41+
const expectedOutput = '<p>This is a long parag…</p>'
42+
expect(st.truncateHTML(htmlIn, maxLength)).toBe(expectedOutput)
43+
})
44+
test('preserves markdown links', () => {
45+
const htmlIn = '<p>This is a [link](http://example.com) to a website.</p>'
46+
const maxLength = 15
47+
const expectedOutput = '<p>This is a [link](http://example.com) to a…</p>'
48+
expect(st.truncateHTML(htmlIn, maxLength)).toBe(expectedOutput)
49+
})
50+
test('adds ellipsis if dots is true', () => {
51+
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
52+
const maxLength = 20
53+
const expectedOutput = '<p>This is a long parag…</p>'
54+
expect(st.truncateHTML(htmlIn, maxLength, true)).toBe(expectedOutput)
55+
})
56+
57+
test('does not add ellipsis if dots is false', () => {
58+
const htmlIn = '<p>This is a long paragraph of text that needs to be truncated.</p>'
59+
const maxLength = 20
60+
const expectedOutput = '<p>This is a long parag</p>'
61+
expect(st.truncateHTML(htmlIn, maxLength, false)).toBe(expectedOutput)
62+
})
63+
})
64+
/*
2765
* changeMarkdownLinksToHTMLLink()
2866
*/
2967
describe('changeMarkdownLinksToHTMLLink()' /* function */, () => {
@@ -73,31 +111,31 @@ describe(`${PLUGIN_NAME}`, () => {
73111
})
74112
test('should produce HTML link 1 with icon and no truncation', () => {
75113
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'
76-
const result = st.changeBareLinksToHTMLLink(input, true, false)
114+
const result = st.changeBareLinksToHTMLLink(input, true)
77115
expect(result).toEqual(
78116
'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')
79117
})
80118
test('should produce HTML link 1 with icon and truncation', () => {
81119
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'
82-
const result = st.changeBareLinksToHTMLLink(input, true, true)
120+
const result = st.changeBareLinksToHTMLLink(input, true, 21)
83121
expect(result).toEqual(
84-
'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')
122+
'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')
85123
})
86124
test('should produce HTML link 1 without icon', () => {
87125
const input = 'this has a https://www.something.com/with?various&chars%20ok valid bare link'
88-
const result = st.changeBareLinksToHTMLLink(input, false, false)
126+
const result = st.changeBareLinksToHTMLLink(input, false)
89127
expect(result).toEqual(
90128
'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')
91129
})
92-
test('should produce HTML link when a link takes up the whole line', () => {
130+
test('should produce HTML link when a link takes up the whole line with icon', () => {
93131
const input = 'https://www.something.com/with?various&chars%20ok'
94-
const result = st.changeBareLinksToHTMLLink(input, true, false)
132+
const result = st.changeBareLinksToHTMLLink(input, true)
95133
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>')
96134
})
97-
test('should produce HTML link when a link takes up the whole line', () => {
98-
const input = 'https://www.something.com/with?various&chars%20ok'
99-
const result = st.changeBareLinksToHTMLLink(input, true, false)
100-
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>')
135+
test('should produce truncated HTML link with a very long bare link', () => {
136+
const input = 'https://validation.poweredbypercent.com/validate/validationinvite_eb574173-f781-4946-b0be-9a06f838289e?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJ0bmVyUHVibGljS2V5IjoicGtfM2YzNzFmMmYtYjQ3MC00M2Q1LTk2MDUtZGMxYTU4YjhjY2IzIiwiaWF0IjoxNzI1NjA5MTkyfQ.GM5ITBbgUHd5Qsyq-d_lkOFIqmTuYJH4Kc4DNIoibE0'
137+
const result = st.changeBareLinksToHTMLLink(input, false, 50)
138+
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>')
101139
})
102140
})
103141

@@ -432,4 +470,29 @@ describe(`${PLUGIN_NAME}`, () => {
432470
})
433471
})
434472
})
473+
474+
describe('removeDateTagsAndToday', () => {
475+
test('should remove ">today at end" ', () => {
476+
expect(st.removeDateTagsAndToday(`test >today`)).toEqual('test')
477+
})
478+
test('should remove ">today at beginning" ', () => {
479+
expect(st.removeDateTagsAndToday(`>today test`)).toEqual(' test')
480+
})
481+
test('should remove ">today in middle" ', () => {
482+
expect(st.removeDateTagsAndToday(`this is a >today test`)).toEqual('this is a test')
483+
})
484+
test('should remove >YYYY-MM-DD date', () => {
485+
expect(st.removeDateTagsAndToday(`test >2021-11-09 `)).toEqual('test')
486+
})
487+
test('should remove nothing if no date tag ', () => {
488+
expect(st.removeDateTagsAndToday(`test no date`)).toEqual('test no date')
489+
})
490+
test('should work for single >week also ', () => {
491+
expect(st.removeDateTagsAndToday(`test >2000-W02`, true)).toEqual('test')
492+
})
493+
test('should work for many items in a line ', () => {
494+
expect(st.removeDateTagsAndToday(`test >2000-W02 >2020-01-01 <2020-02-02 >2020-09-28`, true)).toEqual('test')
495+
})
435496
})
497+
498+
})

0 commit comments

Comments
 (0)