Skip to content

Commit 7736012

Browse files
feat: replace quotes in text
1 parent bfb880b commit 7736012

File tree

3 files changed

+148
-3
lines changed

3 files changed

+148
-3
lines changed

spec/clipboard.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,68 @@ describe('Clipboard', function () {
189189

190190
expect(parseContent(div)[0]).toEqual('bar')
191191
})
192+
193+
// Replace quotation marks
194+
// ------------------
195+
196+
it('replace quotation marks', function () {
197+
const block = extractSingleBlock('text outside "text inside"')
198+
expect(block).toEqual('text outside “text inside”')
199+
})
200+
201+
it('replace nested quotation marks', function () {
202+
const block = extractSingleBlock('text outside "text «inside» text"')
203+
expect(block).toEqual('text outside “text “inside” text”')
204+
})
205+
206+
it('replace multiple nested quotation marks', function () {
207+
const block = extractSingleBlock('text outside "text «inside „double nested“» text"')
208+
expect(block).toEqual('text outside “text “inside “double nested”” text”')
209+
})
210+
211+
it('replace quotation marks and ignore not closing marks', function () {
212+
const block = extractSingleBlock('text outside "text «inside „double nested» text"')
213+
expect(block).toEqual('text outside “text “inside „double nested” text”')
214+
})
215+
216+
it('replace nested quotes with multiple quotes inside nested', function () {
217+
const block = extractSingleBlock('text outside "text «inside» „second inside text“"')
218+
expect(block).toEqual('text outside “text “inside” “second inside text””')
219+
})
220+
221+
it('replace nested quotes with multiple quotes inside nested and not closing marks', function () {
222+
const block = extractSingleBlock('text outside "text «inside» „second «inside» text"')
223+
expect(block).toEqual('text outside “text “inside” „second “inside” text”')
224+
})
225+
226+
it('replace single quotation marks', function () {
227+
const blocks = extract('text outside \'text inside\'')
228+
expect(blocks).toEqual('text outside ‘text inside’')
229+
})
230+
231+
it('replace nested quotes with single quotes inside nested', function () {
232+
const block = extractSingleBlock('text outside "text \'inside\' „second inside text“"')
233+
expect(block).toEqual('text outside “text ‘inside’ “second inside text””')
234+
})
235+
236+
it('replace quotation marks around elements', function () {
237+
const block = extractSingleBlock('text outside "<b>text inside</b>"')
238+
expect(block).toEqual('text outside “<strong>text inside</strong>”')
239+
})
240+
241+
it('replace quotation marks inside elements', function () {
242+
const block = extractSingleBlock('text outside <b>"text inside"</b>')
243+
expect(block).toEqual('text outside <strong>“text inside”</strong>')
244+
})
245+
246+
it('do not replace quotation marks inside tag attributes', function () {
247+
const block = extractSingleBlock('text outside "<a href="https://livingdocs.io">text inside</a>"')
248+
expect(block).toEqual('text outside “<a href="https://livingdocs.io">text inside</a>”')
249+
})
250+
251+
it('replace quotation marks around elements with attributes', function () {
252+
const block = extractSingleBlock('text outside "<a href="https://livingdocs.io">text inside</a>"')
253+
expect(block).toEqual('text outside “<a href="https://livingdocs.io">text inside</a>”')
254+
})
192255
})
193256
})

src/clipboard.js

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ import config from './config'
44
import * as string from './util/string'
55
import * as nodeType from './node-type'
66

7-
let allowedElements, requiredAttributes, transformElements, blockLevelElements
7+
let allowedElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
88
let splitIntoBlocks, blacklistedElements
99
const whitespaceOnly = /^\s*$/
1010
const blockPlaceholder = '<!-- BLOCK -->'
1111
let keepInternalRelativeLinks
12+
const doubleQuotePairs = [
13+
['«', '»'], // ch german, french
14+
['»', '«'], // danish
15+
['"', '"'], // danish, not specified
16+
['“', '”'], // english US
17+
['”', '”'], // swedish
18+
['“', '“'], // chinese simplified
19+
['„', '“'] // german
20+
]
21+
const quotesRegex = /(["'«»])(?![^<]*?>)/g
1222

1323
updateConfig(config)
1424
export function updateConfig (conf) {
@@ -18,6 +28,7 @@ export function updateConfig (conf) {
1828
transformElements = rules.transformElements || {}
1929
blacklistedElements = rules.blacklistedElements || []
2030
keepInternalRelativeLinks = rules.keepInternalRelativeLinks || false
31+
replaceQuotes = rules.replaceQuotes || {}
2132

2233
blockLevelElements = {}
2334
rules.blockLevelElements.forEach((name) => { blockLevelElements[name] = true })
@@ -80,7 +91,7 @@ export function parseContent (element) {
8091
return filterHtmlElements(element)
8192
// Handle Blocks
8293
.split(blockPlaceholder)
83-
.map((entry) => string.trim(cleanWhitespace(entry)))
94+
.map((entry) => string.trim(cleanWhitespace(replaceAllQuotes(entry))))
8495
.filter((entry) => !whitespaceOnly.test(entry))
8596
}
8697

@@ -168,3 +179,69 @@ export function cleanWhitespace (str) {
168179
: ' '
169180
))
170181
}
182+
183+
export function replaceAllQuotes (str) {
184+
const quotes = getAllQuotes(str)
185+
if (quotes && quotes.length > 0) {
186+
const replacementQuotes = getReplacementArray(quotes, 0)
187+
return replaceExistingQuotes(str, replacementQuotes)
188+
}
189+
return str
190+
}
191+
192+
function getReplacementArray (quotes, position) {
193+
const quotesArray = []
194+
if (quotes.length === 1) {
195+
return quotes
196+
}
197+
198+
while (position < quotes.length) {
199+
const closingTagPosition = findClosingQuote(quotes, position)
200+
let nestedArray = []
201+
202+
if (closingTagPosition !== position + 1 && closingTagPosition !== -1 && closingTagPosition !== undefined) {
203+
const nestedquotes = quotes.slice(position + 1, closingTagPosition)
204+
if (nestedquotes) {
205+
nestedArray = getReplacementArray(nestedquotes, 0)
206+
}
207+
}
208+
if (closingTagPosition) {
209+
position = closingTagPosition + 1
210+
}
211+
if (closingTagPosition === undefined || closingTagPosition === -1) {
212+
quotesArray.push(quotes[position])
213+
position++
214+
} else {
215+
quotesArray.push(...[replaceQuotes.quotes[0], ...nestedArray, replaceQuotes.quotes[1]])
216+
}
217+
}
218+
219+
return quotesArray
220+
}
221+
222+
function findClosingQuote (quotes, position) {
223+
const openingQuote = quotes[position]
224+
for (let i = position + 1; i < quotes.length; i++) {
225+
const isIncluded = getPossibleClosingQuotes(openingQuote).includes(quotes[i])
226+
if (isIncluded) {
227+
return i
228+
}
229+
}
230+
}
231+
232+
function getPossibleClosingQuotes (openingQuote) {
233+
return doubleQuotePairs.filter(quotePair => quotePair[0] === openingQuote).map((quotePair) => quotePair[1])
234+
}
235+
236+
function getAllQuotes (str) {
237+
return str.match(quotesRegex)
238+
}
239+
240+
function replaceExistingQuotes (str, replacementQuotes) {
241+
let index = 0
242+
return str.replace(quotesRegex, (match) => {
243+
const replacement = replacementQuotes[index]
244+
index++
245+
return replacement
246+
})
247+
}

src/config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export default {
7575
// and content (text content and child elements) will get removed.
7676
blacklistedElements: ['style', 'script'],
7777

78-
keepInternalRelativeLinks: false
78+
keepInternalRelativeLinks: false,
79+
80+
replaceQuotes: {
81+
quotes: ['“', '”'],
82+
singleQuotes: ['‘', '’']
83+
}
7984
}
8085
}

0 commit comments

Comments
 (0)