Repository: yamatoiizuka/palt-typesetting (ts)

Issue:
Unnecessary Spaces in Safari's Reader Mode
<img width="1143" alt="Screenshot 2024-02-25 at 13 58 44" src="https://github.com/yamatoiizuka/palt-typesetting/assets/47804982/9e060c94-f688-4463-88fd-a8c70ceef45b">
Safari のリーダーモードで、カーニングや四分アキスペースで使われているスペース／NBSP が出力されてしまう。

Pull request:
Remove unnecessary spaces in Safari reader mode
カーニングと四分アキで使用するスペース・NBSP を、CSS の after 擬似要素で出力することで、
Safari のリーダーモードでそれらのスペースが表示されないようにする修正を行いました。

output html
```html
す<span class="typeset-kerning" style="letter-spacing: 0.02em;" data-content="&nbsp;"></span>。
```

typesetting.css
```css
.typeset-thin-space::after,
.typeset-kerning::after {
  content: attr(data-content); /* Space または No-Break Space */
  font-family: ling-one;
  line-height: 0;
}
```

Fix #93

Patch:
diff --git a/src/style/typesetting.css b/src/style/typesetting.css
index c9502fd..ffde6ea 100644
--- a/src/style/typesetting.css
+++ b/src/style/typesetting.css
@@ -1,6 +1,6 @@
 @font-face {
   /* 
-   * カスタムフォント: 1/10000 UPM 幅のスペースとノンブレーキングスペースを定義。
+   * カスタムフォント: 1/10000 UPM 幅の Space と No-Break Space を定義。
    * 注意：完全なゼロ幅スペースを使用すると、Safari では letter-spacing が無視される。
    * @sharapeco による貢献に感謝します。{@link https://gist.github.com/sharapeco/1dd7f4433dad07648e1325e1fa5926ee}
    */
@@ -16,12 +16,17 @@
 
 .typeset-thin-space,
 .typeset-kerning {
-  font-family: ling-one; /* カスタムフォントのスペースを使用します。 */
-  line-height: 0; /* 空白による行の高さの影響を無視します。 */
   letter-spacing: 0; /* 親要素から継承される letter-spacing の影響を無視します。 */
   user-select: none; /* ユーザーによるテキストのコピーを無効にします。 */
 }
 
+.typeset-thin-space::after,
+.typeset-kerning::after {
+  content: attr(data-content); /* Space または No-Break Space */
+  font-family: ling-one; /* カスタムフォントのスペースを使用します。 */
+  line-height: 0; /* 空白による行の高さの影響を無視します。 */
+}
+
 .typeset-no-breaks {
   letter-spacing: 0; /* 親要素から継承される letter-spacing の影響を無視します。 */
 }
diff --git a/src/util-tags.ts b/src/util-tags.ts
index 7b861ea..fa83e70 100644
--- a/src/util-tags.ts
+++ b/src/util-tags.ts
@@ -1,5 +1,7 @@
 import './style/typesetting.css'
 
+const prefix = 'typeset'
+
 /**
  * HTML文書内で単語の区切りを示し、必要に応じて改行の挿入を許可する`<wbr>`タグを生成します。
  * @return `<wbr>`タグを含む文字列。
@@ -10,41 +12,36 @@ const createWbr = (): string => {
 
 /**
  * 四分アキを指定した幅で生成します。この関数は、視覚的なテキストの区切りを改善するために使用されます。
- * @param thisSpaceWidth - 四分アキの幅を指定します（例: "0.2em"）。
- * @param breakable - 改行が許可されているかどうか。trueの場合、空白は通常のスペースとして扱われます。falseの場合、改行を防ぐためにノンブレーキングスペースが使用されます。
- * @return 指定した幅の四分アキを適用したspanタグを含む文字列。
+ * @param thinSpaceWidth - 四分アキの幅を指定します（例: "0.2em"）。
+ * @param breakable - 改行が許可されているかどうか。true の場合、空白は通常の Space として扱われます。false の場合、改行を防ぐために No-Break Space が使用されます。
+ * @return 指定した幅の四分アキを適用した`span`タグを含む文字列。
  */
-const createThinSpace = (thisSpaceWidth: string, breakable?: boolean): string => {
-  const content = breakable ? ' ' : '&nbsp;'
-  const className = 'typeset-thin-space'
-  const style = `letter-spacing: ${thisSpaceWidth};`
-  const uiIgnored = true
-  return createStyledSpan(content, className, style, uiIgnored)
+const createThinSpace = (thinSpaceWidth: string, breakable?: boolean): string => {
+  const content = ''
+  const className = `${prefix}-thin-space`
+  return createSpacer(content, className, thinSpaceWidth, breakable)
 }
 
 /**
  * 指定した数値でカーニング（文字間隔調整）を適用します。カーニングは、テキストの読みやすさを向上させるために使用されます。
  *
  * @param kerningValue - カーニング値（千分率）。1000 は 1em のカーニングを意味します。
- * @param breakable - 改行が許可されているかどうか。trueの場合、空白は通常のスペースとして扱われます。falseの場合、改行を防ぐためにノンブレーキングスペースが使用されます。
- * @return 指定した数値のカーニングを適用したspanタグ。
+ * @param breakable - 改行が許可されているかどうか。true の場合、空白は通常の Space として扱われます。false の場合、改行を防ぐために No-Break Space が使用されます。
+ * @return 指定した数値のカーニングを適用した`span`タグ。
  */
 const createKerning = (kerningValue: number, breakable?: boolean): string => {
   if (kerningValue === 0) return ''
 
-  const className = 'typeset-kerning'
-  const uiIgnored = true
+  const content = ''
+  const className = `${prefix}-kerning`
 
   if (kerningValue < 0) {
-    const content = ''
     const emValue = kerningValue / 1000 / 2 + 'em' // margin は上下左右にかかるので、1/2 にする
     const style = `margin: ${emValue};`
-    return createStyledSpan(content, className, style, uiIgnored)
+    return createStyledSpan(content, className, style)
   } else {
-    const content = breakable ? ' ' : '&nbsp;'
     const emValue = kerningValue / 1000 + 'em'
-    const style = `letter-spacing: ${emValue};`
-    return createStyledSpan(content, className, style, uiIgnored)
+    return createSpacer(content, className, emValue, breakable)
   }
 }
 
@@ -52,11 +49,11 @@ const createKerning = (kerningValue: number, breakable?: boolean): string => {
  * 単語区切りでの改行を行う場合にのみ、与えられたテキストに`typeset-word-break`クラスを適用します。
  * @param text - クラスを適用するテキスト。
  * @param useWordBreak - 単語区切りでの改行を行うかどうか。
- * @return クラス適用されたテキストを含むspanタグ。
+ * @return クラス適用されたテキストを含む`span`タグ。
  */
 const applyWrapperStyle = (text: string, useWordBreak: boolean): string => {
-  const wrapperName = 'typeset'
-  const wordBreakName = 'typeset-word-break'
+  const wrapperName = prefix
+  const wordBreakName = `${prefix}-word-break`
   const className = useWordBreak ? `${wrapperName} ${wordBreakName}` : wrapperName
   return createStyledSpan(text, className)
 }
@@ -67,39 +64,50 @@ const applyWrapperStyle = (text: string, useWordBreak: boolean): string => {
  * @return クラス適用されたセグメントを含むspanタグを返します。
  */
 const applyLatinStyle = (segment: string): string => {
-  const className = 'typeset-latin'
+  const className = `${prefix}-latin`
   return createStyledSpan(segment, className)
 }
 
 /**
  * 与えられたセグメントに`typeset-no-breaks`クラスを適用します。このクラスは、指定されたセグメント内の`letter-spacing`を0にするために使用されます。
  * @param segment - クラスを適用するセグメント。
- * @return クラス適用されたセグメントを含むspanタグを返します。
+ * @return クラス適用されたセグメントを含む`span`タグを返します。
  */
 const applyNoBreaksStyle = (segment: string): string => {
-  const className = 'typeset-no-breaks'
+  const className = `${prefix}-no-breaks`
   return createStyledSpan(segment, className)
 }
 
 /**
  * スタイリングされた`span`タグを生成し、指定された内容とクラス名を適用します。オプショナルでスタイルとUI無視の属性も設定可能です。
  *
- * @param content - `span`タグ内に表示される内容。改行を防ぎたい場合は`&nbsp;`を、可能であれば普通の空白`' '`を使用します。
+ * @param content - `span`タグ内に表示される内容。
  * @param className - この`span`タグに適用するCSSクラス名。
- * @param style - `span`タグに適用するインラインスタイル（オプショナル）。デフォルトは空文字列で、スタイルが不要な場合に使用します。
- * @param uiIgnored - `true`の場合、`aria-hidden="true"`と`data-nosnippet=""`属性が追加され、UIには表示されず検索エンジンのスニペットからも除外されます。
- *                  これは主にアクセシビリティやSEOの目的で使用されます。デフォルトは`false`です。
+ * @param style - `span`タグに適用するインラインスタイル（オプショナル）。デフォルトは空文字列で、スタイルが必要な場合に使用します。
+ * @param attr - `span`タグに適用する属性（オプショナル）。デフォルトは空文字列で、class/style 以外の属性が必要な場合に使用します。
  * @return 指定されたパラメータに基づいて構築された`span`タグを含む文字列。
  */
-const createStyledSpan = (
-  content: string,
-  className: string,
-  style: string = '',
-  uiIgnored: boolean = false
-): string => {
+const createStyledSpan = (content: string, className: string, style: string = '', attr: string = ''): string => {
   const styleAttr = style ? ` style="${style}"` : ''
-  const extraAttrs = uiIgnored ? ' aria-hidden="true" data-nosnippet=""' : ''
-  return `<span class="${className}"${styleAttr}${extraAttrs}>${content}</span>`
+  return `<span class="${className}"${styleAttr}${attr}>${content}</span>`
+}
+
+/**
+ * 指定された値でスペーシングを適用した`span`タグを生成します。この関数は、テキスト間の視覚的な空白を管理し、
+ * テキストの区切りや読みやすさを向上させるために使用されます。また、改行の挙動を制御するオプションを提供し、
+ * 改行が許可されている場合は通常の空白（Space）、許可されていない場合は改行を防ぐ空白（No-Break Space）を
+ * 使用します。
+ *
+ * @param content - `span`タグ内に表示される内容。この関数では主に空文字列が使用されます。
+ * @param className - `span`タグに適用する CSS クラス名。スタイリングのために使用されます。
+ * @param value - `letter-spacing`の値として適用するスペーシングの幅。例: "0.2em"。
+ * @param breakable - 改行が許可されているかどうか。true の場合、スペーサーは改行可能とみなされ、false の場合は改行不可。
+ * @return 指定されたスタイリングと挙動を持つ`span`タグを含む文字列。
+ */
+const createSpacer = (content: string, className: string, value: string, breakable?: boolean): string => {
+  const style = `letter-spacing: ${value};`
+  const data = ` data-content="${breakable ? ' ' : '&nbsp;'}"`
+  return createStyledSpan(content, className, style, data)
 }
 
 export { createWbr, createThinSpace, createKerning, applyWrapperStyle, applyLatinStyle, applyNoBreaksStyle }


Test patch:
diff --git a/tests/apply-style.test.ts b/tests/apply-style.test.ts
index f1ec8df..a7cd71b 100644
--- a/tests/apply-style.test.ts
+++ b/tests/apply-style.test.ts
@@ -86,16 +86,14 @@ describe('applyStyleToSegment', () => {
   it("adds kerning tag after 'で'", () => {
     const current = 'す'
     const next = '。'
-    const expected =
-      'す<span class="typeset-kerning" style="margin: -0.04em;" aria-hidden="true" data-nosnippet=""></span>'
+    const expected = 'す<span class="typeset-kerning" style="margin: -0.04em;"></span>'
     expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
   it("adds kerning tag after 'で'", () => {
     const current = 'です。'
     const next = 'その'
-    const expected =
-      'です<span class="typeset-kerning" style="margin: -0.04em;" aria-hidden="true" data-nosnippet=""></span>。'
+    const expected = 'です<span class="typeset-kerning" style="margin: -0.04em;"></span>。'
     expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 })
@@ -125,7 +123,7 @@ describe('applyStyleToSegment without useWordBreak option', () => {
     const space = ' '
     const current = 'し'
     const next = 'ます。'
-    const expected = `し<span class="typeset-kerning" style="letter-spacing: 0.06em;" aria-hidden="true" data-nosnippet="">${space}</span>`
+    const expected = `し<span class="typeset-kerning" style="letter-spacing: 0.06em;" data-content="${space}"></span>`
     expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
@@ -133,7 +131,7 @@ describe('applyStyleToSegment without useWordBreak option', () => {
     const space = '&nbsp;'
     const current = 'です'
     const next = '。'
-    const expected = `です<span class="typeset-kerning" style="letter-spacing: 0.02em;" aria-hidden="true" data-nosnippet="">${space}</span>`
+    const expected = `です<span class="typeset-kerning" style="letter-spacing: 0.02em;" data-content="${space}"></span>`
     expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 
@@ -141,7 +139,7 @@ describe('applyStyleToSegment without useWordBreak option', () => {
     const space = '&nbsp;'
     const current = 'Java'
     const next = 'Script'
-    const expected = `Java<span class="typeset-kerning" style="letter-spacing: 0.02em;" aria-hidden="true" data-nosnippet="">${space}</span>`
+    const expected = `Java<span class="typeset-kerning" style="letter-spacing: 0.02em;" data-content="${space}"></span>`
     expect(applyKerningToSegment(current, next, options)).toEqual(expected)
   })
 })