Repository: yamatoiizuka/palt-typesetting (ts)

Issue:
Do not insert thin spaces before punctuation in Latin text
**as is**
`<span class="typeset-latin">English</span><span class="typeset-thin-space"> </span>。`

**to be**
`<span class="typeset-latin">English</span>。`
Fix isLatin(), isNoBreaks() 
when `useWordBreak === false && insertThinSpaces === false`,
and `input = "Palt Typesetting は、美しい組版を作ります。"`

**as is**
`<typeset-latin>Palt Typesettingは、美しい組版を作ります。</typeset-latin>`

**to be**
`<typeset-latin>Palt Typesetting</typeset-latin>は、美しい組版を作ります。`

Pull request:
Fix language wrap functions
- Fix #19
- Fix #20

Patch:
diff --git a/src/apply-style.ts b/src/apply-style.ts
index 5bce9e1..1e333e2 100644
--- a/src/apply-style.ts
+++ b/src/apply-style.ts
@@ -1,6 +1,7 @@
 import { isBreakable } from './insert-separators'
 import { CharClass, LanguageClass } from './util-text-classes'
 import { applyWrapperStyle, applyLatinStyle, applyNoBreaksStyle, createKerning } from './util-tags'
+import * as util from './util-regex'
 import { TypesettingOptions } from './types'
 
 /**
@@ -32,19 +33,17 @@ const applyStyleToSegment = (currentSegment: string, nextSegment: string, option
     return currentSegment
   }
 
-  const kernedSegment = applyKerningToSegment(currentSegment, nextSegment, options)
-
   // ラテン文字のセグメントには 'latin' クラスを適用
   if (options.wrapLatin && LanguageClass.isLatin(currentSegment)) {
-    return applyLatinStyle(kernedSegment)
+    return currentSegment.replace(util.latinRegex, match => applyLatinStyle(match))
   }
 
   // 改行をしないセグメントにはゼロの文字間隔スタイルを適用
   if (options.noSpaceBetweenNoBreaks && CharClass.shouldNotBreak(currentSegment)) {
-    return applyNoBreaksStyle(kernedSegment)
+    return currentSegment.replace(util.noBreakRulesRegex, match => applyNoBreaksStyle(match))
   }
 
-  return kernedSegment
+  return currentSegment
 }
 
 /**
@@ -76,4 +75,4 @@ const applyKerningToSegment = (currentSegment: string, nextSegment: string, opti
   return kernedChars.join('')
 }
 
-export { applyStyleToText, applyStyleToSegment }
+export { applyStyleToText, applyStyleToSegment, applyKerningToSegment }
diff --git a/src/index.ts b/src/index.ts
index 45174de..5a40d0c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
 import HTMLProcessor from './html-processor'
 import insertSeparatorsToText from './insert-separators'
-import { applyStyleToText, applyStyleToSegment } from './apply-style'
+import { applyStyleToText, applyStyleToSegment, applyKerningToSegment } from './apply-style'
 import win from './win'
 import type { TypesettingOptions, KerningRule } from './types'
 
@@ -29,7 +29,7 @@ class Typesetter extends HTMLProcessor {
   private isIntlSegmenterSupported: boolean
 
   constructor(options: Partial<TypesettingOptions> = {}) {
-    const transformFunctions = [applyStyleToText, insertSeparatorsToText, applyStyleToSegment]
+    const transformFunctions = [applyStyleToText, insertSeparatorsToText, applyStyleToSegment, applyKerningToSegment]
     const validatedOptions = Typesetter.validateOptions(options)
 
     super(transformFunctions, validatedOptions)
diff --git a/src/util-regex.ts b/src/util-regex.ts
index adadc5a..4b00f47 100644
--- a/src/util-regex.ts
+++ b/src/util-regex.ts
@@ -14,15 +14,25 @@ export const japaneseRegex = /[\p{scx=Hiragana}\p{scx=Katakana}|ｦ-ﾟ\p{scx=Ha
 export const openingsRegex = /（|［|｛|〔|〈|《|「|『|【|〘|〖|“|‘/
 
 /**
- * 終わり括弧類、句点類、読点類
+ * 終わり括弧類
  */
-export const closingsRegex = /）|］|｝|〕|〉|》|」|』|】|〙|〗|”|’|、|，|。|．/
+export const closingsRegex = /）|］|｝|〕|〉|》|」|』|】|〙|〗|”|’/
 
 /**
  * 中点類
  */
 export const middleDotsRegex = /・|：|；/
 
+/**
+ * 読点類
+ */
+export const commasRegex = /、|，/
+
+/**
+ * 句点類
+ */
+export const periodsRegex = /。|．/
+
 /**
  * 分離禁則文字
  */
diff --git a/src/util-text-classes.ts b/src/util-text-classes.ts
index 467a47a..f2798f5 100644
--- a/src/util-text-classes.ts
+++ b/src/util-text-classes.ts
@@ -1,4 +1,4 @@
-import * as utils from './util-regex'
+import * as util from './util-regex'
 
 /**
  * 文字クラス関連の処理を提供するクラスです。
@@ -12,7 +12,7 @@ const CharClass = {
    * @return 分離禁則文字で構成されている場合はtrue、そうでない場合はfalse
    */
   shouldNotBreak: (segment: string): boolean => {
-    return utils.noBreakRulesRegex.test(segment)
+    return util.noBreakRulesRegex.test(segment)
   },
 
   /**
@@ -22,19 +22,23 @@ const CharClass = {
    * @return 四分アキを追加すべきかどうか
    */
   shouldAddThinSpace: (current: string, next: string): boolean => {
+    const spaceAfterRegex = new RegExp(
+      `(${util.closingsRegex.source}|${util.commasRegex.source}|${util.periodsRegex.source})`
+    )
+
     const settings = {
-      openings: {
-        regex: utils.openingsRegex,
+      spaceBefore: {
+        regex: util.openingsRegex,
         hasSpaceBefore: true,
         hasSpaceAfter: false,
       },
-      closings: {
-        regex: utils.closingsRegex,
+      spaceAfter: {
+        regex: spaceAfterRegex,
         hasSpaceBefore: false,
         hasSpaceAfter: true,
       },
-      middleDots: {
-        regex: utils.middleDotsRegex,
+      spaceBoth: {
+        regex: util.middleDotsRegex,
         hasSpaceBefore: true,
         hasSpaceAfter: true,
       },
@@ -53,6 +57,17 @@ const CharClass = {
 
     return false
   },
+
+  /**
+   * 与えられた文字列の最初の文字が句読点に一致するかどうか判断します。
+   * @param {string} segment - 判断対象の文字列
+   * @return {boolean} 最初の文字が句読点であればtrueを返します。
+   */
+  startsWithPunctuation: (segment: string): boolean => {
+    // periodsRegexとcommasRegexを組み合わせて一つの正規表現を作成
+    const combinedRegex = new RegExp(`^[${util.periodsRegex.source}${util.commasRegex.source}]`)
+    return combinedRegex.test(segment)
+  },
 }
 
 /**
@@ -67,7 +82,7 @@ const LanguageClass = {
    * @return ラテン文字で構成されている場合はtrue、そうでない場合はfalse
    */
   isLatin: (segment: string): boolean => {
-    return utils.latinRegex.test(segment)
+    return util.latinRegex.test(segment)
   },
 
   /**
@@ -76,7 +91,7 @@ const LanguageClass = {
    * @return 日本語文字で構成されている場合はtrue、そうでない場合はfalse
    */
   isJapanese: (segment: string): boolean => {
-    return utils.japaneseRegex.test(segment)
+    return util.japaneseRegex.test(segment)
   },
 
   /**
@@ -99,7 +114,7 @@ const LanguageClass = {
    * @return 四分アキを追加すべきかどうか
    */
   shouldAddThinSpace: (current: string, next: string): boolean => {
-    return LanguageClass.isDifferentLanguageClass(current, next)
+    return LanguageClass.isDifferentLanguageClass(current, next) && !CharClass.startsWithPunctuation(next)
   },
 }
 


Test patch:
diff --git a/tests/apply-style.test.ts b/tests/apply-style.test.ts
index 9bf01fb..5271726 100644
--- a/tests/apply-style.test.ts
+++ b/tests/apply-style.test.ts
@@ -1,5 +1,5 @@
 import Typesetter from '../src'
-import { applyStyleToSegment } from '../src/apply-style'
+import { applyStyleToSegment, applyKerningToSegment } from '../src/apply-style'
 import { TypesettingOptions } from '../src/types'
 import { describe, it, expect } from 'vitest'
 
@@ -68,7 +68,7 @@ describe('applyStyleToSegment', () => {
     const next = '。'
     const expected =
       'す<span class="typeset-kerning" style="margin: -0.04em;" aria-hidden="true" data-nosnippet=""></span>'
-    expect(applyStyleToSegment(current, next, options)).toEqual(expected)
+    expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
   it("adds kerning tag after 'で'", () => {
@@ -76,7 +76,7 @@ describe('applyStyleToSegment', () => {
     const next = 'その'
     const expected =
       'です<span class="typeset-kerning" style="margin: -0.04em;" aria-hidden="true" data-nosnippet=""></span>。'
-    expect(applyStyleToSegment(current, next, options)).toEqual(expected)
+    expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 })
 
@@ -106,7 +106,7 @@ describe('applyStyleToSegment without useWordBreak option', () => {
     const current = 'し'
     const next = 'ます。'
     const expected = `し<span class="typeset-kerning" style="letter-spacing: 0.06em;" aria-hidden="true" data-nosnippet="">${space}</span>`
-    expect(applyStyleToSegment(current, next, options)).toEqual(expected)
+    expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
   it("adds unbreakable kerning tag after 'す'", () => {
@@ -114,14 +114,14 @@ describe('applyStyleToSegment without useWordBreak option', () => {
     const current = 'です'
     const next = '。'
     const expected = `です<span class="typeset-kerning" style="letter-spacing: 0.02em;" aria-hidden="true" data-nosnippet="">${space}</span>`
-    expect(applyStyleToSegment(current, next, options)).toEqual(expected)
+    expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
   it("adds unbreakable kerning tag after 'Java'", () => {
     const space = '&nbsp;'
     const current = 'Java'
     const next = 'Script'
-    const expected = `<span class="typeset-latin">Java<span class="typeset-kerning" style="letter-spacing: 0.02em;" aria-hidden="true" data-nosnippet="">${space}</span></span>`
-    expect(applyStyleToSegment(current, next, options)).toEqual(expected)
+    const expected = `Java<span class="typeset-kerning" style="letter-spacing: 0.02em;" aria-hidden="true" data-nosnippet="">${space}</span>`
+    expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 })
diff --git a/tests/index.test.ts b/tests/index.test.ts
index ec4a12d..0912ff6 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -12,7 +12,7 @@ describe('Typesetter', () => {
   const wbr = createWbr()
   const srcHtml = `<p>──<b>こんにちは。</b>「日本語」とEnglish、晴れ・28度。</p>`
 
-  const expectedHtml = `<p>${applyWrapperStyle(`${applyNoBreaksStyle('──')}${wbr}`, true)}<b>${applyWrapperStyle(`こんにちは。${space}`, true)}</b>${applyWrapperStyle(`「日本語」${space}と${space}${applyLatinStyle('English')}${nbsp}、${space}晴れ${nbsp}・${space}${applyLatinStyle('28')}${space}度。`, true)}</p>`
+  const expectedHtml = `<p>${applyWrapperStyle(`${applyNoBreaksStyle('──')}${wbr}`, true)}<b>${applyWrapperStyle(`こんにちは。${space}`, true)}</b>${applyWrapperStyle(`「日本語」${space}と${space}${applyLatinStyle('English')}、${space}晴れ${nbsp}・${space}${applyLatinStyle('28')}${space}度。`, true)}</p>`
 
   const typeset = new Typesetter()
 
diff --git a/tests/insert-separators.test.ts b/tests/insert-separators.test.ts
index ba685ff..a902876 100644
--- a/tests/insert-separators.test.ts
+++ b/tests/insert-separators.test.ts
@@ -17,7 +17,7 @@ describe('insertSeparators', () => {
   it('inserts separators (thin spaces, <wbr>) into HTML text nodes', () => {
     const currentText = '──「こんにちは。」日本語とEnglish、晴れ・28度à vous。'
     const nextText = '「合成フォント」の見本。'
-    const expected = `──${nbsp}「こんにちは。」${space}日本語${wbr}と${space}English${nbsp}、${space}晴れ${nbsp}・${space}28${space}度${space}à ${wbr}vous${nbsp}。${space}`
+    const expected = `──${nbsp}「こんにちは。」${space}日本語${wbr}と${space}English、${space}晴れ${nbsp}・${space}28${space}度${space}à ${wbr}vous。${space}`
     expect(insertSeparatorsToText(currentText, nextText, options)).toEqual(expected)
   })
 })
@@ -90,7 +90,7 @@ describe('addSeparatorsToSegment', () => {
     {
       current: 'English',
       next: '、',
-      expected: 'English' + nbsp,
+      expected: 'English',
     },
     {
       current: '、',
@@ -130,7 +130,7 @@ describe('addSeparatorsToSegment', () => {
     {
       current: 'vous',
       next: '。',
-      expected: 'vous' + nbsp,
+      expected: 'vous',
     },
   ]
 
@@ -187,7 +187,7 @@ describe('addSeparatorsToSegment without useWordBreak', () => {
     {
       current: 'English',
       next: '、',
-      expected: 'English' + nbsp,
+      expected: 'English',
     },
     {
       current: '、',
@@ -227,7 +227,7 @@ describe('addSeparatorsToSegment without useWordBreak', () => {
     {
       current: 'vous',
       next: '。',
-      expected: 'vous' + nbsp,
+      expected: 'vous',
     },
   ]
 
@@ -276,7 +276,7 @@ describe('shouldAddThinSpace', () => {
     { current: '」', next: '日本語', expected: true },
     { current: '日本語', next: 'と', expected: false },
     { current: 'と', next: 'English', expected: true },
-    { current: 'English', next: '、', expected: true },
+    { current: 'English', next: '、', expected: false },
     { current: '、', next: '晴れ', expected: true },
     { current: '晴れ', next: '・', expected: true },
     { current: '・', next: '28', expected: true },
@@ -284,7 +284,7 @@ describe('shouldAddThinSpace', () => {
     { current: '度', next: 'à', expected: true },
     { current: 'à', next: ' ', expected: false },
     { current: ' ', next: 'vous', expected: false },
-    { current: 'vous', next: '。', expected: true },
+    { current: 'vous', next: '。', expected: false },
   ]
 
   tests.forEach(({ current, next, expected }) => {
diff --git a/tests/text-classification.test.ts b/tests/text-classification.test.ts
index c59f327..fca81f6 100644
--- a/tests/text-classification.test.ts
+++ b/tests/text-classification.test.ts
@@ -36,7 +36,7 @@ describe('LanguageClass.shouldAddThinSpace', () => {
     { current: '」', next: '日本語', expected: false },
     { current: '日本語', next: 'と', expected: false },
     { current: 'と', next: 'English', expected: true },
-    { current: 'English', next: '、', expected: true },
+    { current: 'English', next: '、', expected: false },
     { current: '、', next: '晴れ', expected: false },
     { current: '晴れ', next: '・', expected: false },
     { current: '・', next: '28', expected: true },