Skip to content

Commit 23fb7f7

Browse files
fix: single quotes and apostrophe
1 parent 7736012 commit 23fb7f7

File tree

4 files changed

+143
-74
lines changed

4 files changed

+143
-74
lines changed

spec/clipboard.spec.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ describe('Clipboard', function () {
193193
// Replace quotation marks
194194
// ------------------
195195

196+
it('do nothting when replaceQuotes is not set', function () {
197+
const updatedConfig = cloneDeep(config)
198+
updatedConfig.pastedHtmlRules.replaceQuotes = undefined
199+
200+
updateConfig(updatedConfig)
201+
const block = extractSingleBlock('text outside "text inside"')
202+
expect(block).toEqual('text outside "text inside"')
203+
})
204+
196205
it('replace quotation marks', function () {
197206
const block = extractSingleBlock('text outside "text inside"')
198207
expect(block).toEqual('text outside “text inside”')
@@ -223,16 +232,31 @@ describe('Clipboard', function () {
223232
expect(block).toEqual('text outside “text “inside” „second “inside” text”')
224233
})
225234

235+
it('replace apostrophe', function () {
236+
const block = extractSingleBlock(`don't`)
237+
expect(block).toEqual('don’t')
238+
})
239+
240+
it('replace apostrophe inside quotes', function () {
241+
const block = extractSingleBlock(`outside "don't"`)
242+
expect(block).toEqual('outside “don’t”')
243+
})
244+
226245
it('replace single quotation marks', function () {
227-
const blocks = extract('text outside \'text inside\'')
228-
expect(blocks).toEqual('text outside ‘text inside’')
246+
const block = extractSingleBlock('text outside \'text inside\'')
247+
expect(block).toEqual('text outside ‘text inside’')
229248
})
230249

231250
it('replace nested quotes with single quotes inside nested', function () {
232251
const block = extractSingleBlock('text outside "text \'inside\' „second inside text“"')
233252
expect(block).toEqual('text outside “text ‘inside’ “second inside text””')
234253
})
235254

255+
it('replace nested quotes with single quotes inside nested', function () {
256+
const block = extractSingleBlock(`text outside "text 'inside „second inside text“'"`)
257+
expect(block).toEqual('text outside “text ‘inside “second inside text”’”')
258+
})
259+
236260
it('replace quotation marks around elements', function () {
237261
const block = extractSingleBlock('text outside "<b>text inside</b>"')
238262
expect(block).toEqual('text outside “<strong>text inside</strong>”')

src/clipboard.js

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,13 @@ import $ from 'jquery'
33
import config from './config'
44
import * as string from './util/string'
55
import * as nodeType from './node-type'
6+
import * as quotes from './quotes'
67

78
let allowedElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
89
let splitIntoBlocks, blacklistedElements
910
const whitespaceOnly = /^\s*$/
1011
const blockPlaceholder = '<!-- BLOCK -->'
1112
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
2213

2314
updateConfig(config)
2415
export function updateConfig (conf) {
@@ -181,67 +172,9 @@ export function cleanWhitespace (str) {
181172
}
182173

183174
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)
175+
if (replaceQuotes.quotes) {
176+
return quotes.replaceAllQuotes(str, replaceQuotes)
188177
}
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 = []
201178

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-
})
179+
return str
247180
}

src/config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export default {
7979

8080
replaceQuotes: {
8181
quotes: ['“', '”'],
82-
singleQuotes: ['‘', '’']
82+
singleQuotes: ['‘', '’'],
83+
apostrophe: '’'
8384
}
8485
}
8586
}

src/quotes.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const doubleQuotePairs = [
2+
['«', '»'], // ch german, french
3+
['»', '«'], // danish
4+
['"', '"'], // danish, not specified
5+
['“', '”'], // english US
6+
['”', '”'], // swedish
7+
['“', '“'], // chinese simplified
8+
['„', '“'] // german
9+
]
10+
const singleQuotePairs = [
11+
['‘', '’'], // english UK
12+
['‹', '›'], // ch german, french
13+
['‚', '‘'], // german
14+
['’', '’'], // swedish
15+
['›', '‹'], // danish
16+
[`'`, `'`], // danish, not specified
17+
[`‘`, `’`] // chinese simplified
18+
]
19+
20+
const apostrophe = [
21+
'’', // german
22+
`'` // default
23+
]
24+
const quotesRegex = /(['«»"])(?![^<]*?>)/g
25+
let replaceQuotes
26+
27+
export function replaceAllQuotes (str, replaceQuotesRules) {
28+
replaceQuotes = replaceQuotesRules
29+
const quotes = getAllQuotes(str)
30+
if (quotes && quotes.length > 0) {
31+
const replacementQuotes = getReplacementArray(quotes, 0)
32+
return replaceExistingQuotes(str, replacementQuotes)
33+
}
34+
35+
return str
36+
}
37+
38+
function getReplacementArray (quotes, position) {
39+
const quotesArray = []
40+
if (quotes.length === 1) {
41+
const replacedQuote = replaceApostrophe(quotes[0])
42+
return [replacedQuote]
43+
}
44+
45+
while (position < quotes.length) {
46+
const closingTag = findClosingQuote(quotes, position)
47+
let nestedArray = []
48+
49+
if (closingTag !== undefined && closingTag.position !== position + 1 && closingTag.position !== -1) {
50+
const nestedquotes = quotes.slice(position + 1, closingTag.position)
51+
if (nestedquotes) {
52+
nestedArray = getReplacementArray(nestedquotes, 0)
53+
}
54+
}
55+
56+
if (closingTag === undefined || closingTag.position === -1) {
57+
const replacedQuote = replaceApostrophe(quotes[position])
58+
quotesArray.push(replacedQuote)
59+
position++
60+
} else {
61+
position = closingTag.position + 1
62+
if (closingTag.type === 'double') {
63+
quotesArray.push(...[replaceQuotes.quotes[0], ...nestedArray, replaceQuotes.quotes[1]])
64+
}
65+
if (closingTag.type === 'single') {
66+
quotesArray.push(...[replaceQuotes.singleQuotes[0], ...nestedArray, replaceQuotes.singleQuotes[1]])
67+
}
68+
}
69+
}
70+
71+
return quotesArray
72+
}
73+
74+
function findClosingQuote (quotes, position) {
75+
const openingQuote = quotes[position]
76+
const possibleClosingSingleQuotes = getPossibleClosingQuotes(openingQuote, singleQuotePairs)
77+
const possibleClosingDoubleQuotes = getPossibleClosingQuotes(openingQuote, doubleQuotePairs)
78+
for (let i = position + 1; i < quotes.length; i++) {
79+
if (possibleClosingSingleQuotes.includes(quotes[i])) {
80+
return {position: i, type: 'single'}
81+
}
82+
if (possibleClosingDoubleQuotes.includes(quotes[i])) {
83+
return {position: i, type: 'double'}
84+
}
85+
}
86+
}
87+
88+
function getPossibleClosingQuotes (openingQuote, pairs) {
89+
return pairs.filter(quotePair => quotePair[0] === openingQuote).map((quotePair) => quotePair[1])
90+
}
91+
92+
function replaceApostrophe (quote) {
93+
if (apostrophe.includes(quote)) {
94+
return replaceQuotes.apostrophe
95+
}
96+
97+
return quote
98+
}
99+
100+
function getAllQuotes (str) {
101+
return str.match(quotesRegex)
102+
}
103+
104+
function replaceExistingQuotes (str, replacementQuotes) {
105+
let index = 0
106+
return str.replace(quotesRegex, (match) => {
107+
const replacement = replacementQuotes[index]
108+
index++
109+
return replacement
110+
})
111+
}

0 commit comments

Comments
 (0)