diff --git a/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap b/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap index 1445dbfba029f..824fd17e28055 100644 --- a/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap +++ b/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap @@ -679,7 +679,7 @@ Object { exports[`Wordcount and timeToRead are generated correctly from schema correctly generate timeToRead for CJK 1`] = ` Object { - "timeToRead": 2, + "timeToRead": 1, } `; diff --git a/packages/gatsby-transformer-remark/src/__tests__/extend-node.js b/packages/gatsby-transformer-remark/src/__tests__/extend-node.js index 4520af2351d2d..5e57617d6faf2 100644 --- a/packages/gatsby-transformer-remark/src/__tests__/extend-node.js +++ b/packages/gatsby-transformer-remark/src/__tests__/extend-node.js @@ -867,18 +867,13 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid 以声明式编写 UI,可以让你的代码更加可靠,且方便调试。 创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。 组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。 - -React は、インタラクティブなユーザーインターフェイスの作成にともなう苦痛を取り除きます。アプリケーションの各状態に対応するシンプルな View を設計するだけで、React はデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。 -宣言的な View を用いてアプリケーションを構築することで、コードはより見通しが立ちやすく、デバッグのしやすいものになります。 -自分自身の状態を管理するカプセル化されたコンポーネントをまず作成し、これらを組み合わせることで複雑なユーザーインターフェイスを構築します。 -コンポーネントのロジックは、Template ではなく JavaScript そのもので書くことができるので、様々なデータをアプリケーション内で簡単に取り回すことができ、かつ DOM に状態を持たせないようにすることができます。 - - React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다. 애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요. 그럼 React는 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링합니다.선언형 뷰는 코드를 예측 가능하고 디버그하기 쉽게 만들어 줍니다. +宣言的な View React は、インタラクティブなユーザインターフェイスの作成にともなう苦痛を取り除きます。アプリケーションの各状態に対応するシンプルな View を設計するだけで、React はデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。 宣言的な View を用いてアプリケーションを構築することで、コードはより見通しが立ちやすく、デバッグのしやすいものになります。 +コンポーネントベース 自分自身の状態を管理するカプセル化されたコンポーネントをまず作成し、これらを組み合わせることで複雑なユーザインターフェイスを構築します。 コンポーネントのロジックは、Template ではなく JavaScript そのもので書くことができるので、様々なデータをアプリケーション内で簡単に取り回すことができ、かつ DOM に状態を持たせないようにすることができます。 `, `timeToRead`, node => { expect(node).toMatchSnapshot() - expect(node.timeToRead).toEqual(2) + expect(node.timeToRead).toEqual(1) } ) diff --git a/packages/gatsby-transformer-remark/src/extend-node-type.js b/packages/gatsby-transformer-remark/src/extend-node-type.js index 5dc44ddd0be34..2d716f16b95bc 100644 --- a/packages/gatsby-transformer-remark/src/extend-node-type.js +++ b/packages/gatsby-transformer-remark/src/extend-node-type.js @@ -1,6 +1,5 @@ const Remark = require(`remark`) const select = require(`unist-util-select`) -const sanitizeHTML = require(`sanitize-html`) const _ = require(`lodash`) const visit = require(`unist-util-visit`) const toHAST = require(`mdast-util-to-hast`) @@ -22,6 +21,7 @@ const { findLastTextNode, } = require(`./hast-processing`) const codeHandler = require(`./code-handler`) +const { timeToRead } = require(`./utils/time-to-read`) let fileNodes let pluginsCacheStr = `` @@ -604,20 +604,7 @@ module.exports = ( timeToRead: { type: `Int`, resolve(markdownNode) { - return getHTML(markdownNode).then(html => { - let timeToRead = 0 - const pureText = sanitizeHTML(html, { allowTags: [] }) - const avgWPM = 265 - const wordCount = - _.words(pureText).length + - _.words(pureText, /[\p{sc=Katakana}\p{sc=Hiragana}\p{sc=Han}]/gu) - .length - timeToRead = Math.round(wordCount / avgWPM) - if (timeToRead === 0) { - timeToRead = 1 - } - return timeToRead - }) + return getHTML(markdownNode).then(timeToRead) }, }, tableOfContents: { diff --git a/packages/gatsby-transformer-remark/src/utils/time-to-read.js b/packages/gatsby-transformer-remark/src/utils/time-to-read.js new file mode 100644 index 0000000000000..d03f15cf8cffe --- /dev/null +++ b/packages/gatsby-transformer-remark/src/utils/time-to-read.js @@ -0,0 +1,67 @@ +const sanitizeHTML = require(`sanitize-html`) +const _ = require(`lodash`) + +// Unicode ranges for Han (Chinese) and Hiragana/Katakana (Japanese) characters +const cjRanges = [ + [11904, 11930], // Han + [11931, 12020], + [12032, 12246], + [12293, 12294], + [12295, 12296], + [12321, 12330], + [12344, 12348], + [13312, 19894], + [19968, 40939], + [63744, 64110], + [64112, 64218], + [131072, 173783], + [173824, 177973], + [177984, 178206], + [178208, 183970], + [183984, 191457], + [194560, 195102], + [12353, 12439], // Hiragana + [12445, 12448], + [110593, 110879], + [127488, 127489], + [12449, 12539], // Katakana + [12541, 12544], + [12784, 12800], + [13008, 13055], + [13056, 13144], + [65382, 65392], + [65393, 65438], + [110592, 110593], +] + +function isCjChar(char) { + const charCode = char.codePointAt(0) + return cjRanges.some(([from, to]) => charCode >= from && charCode < to) +} + +export const timeToRead = html => { + let timeToRead = 0 + const pureText = sanitizeHTML(html, { allowTags: [] }) + const avgWPM = 265 + + let latinChars = [] + let cjChars = [] + + for (const char of pureText) { + if (isCjChar(char)) { + cjChars.push(char) + } else { + latinChars.push(char) + } + } + + // Multiply non-latin character string length by 0.56, because + // on average one word consists of 2 characters in both Chinese and Japanese + const wordCount = _.words(latinChars.join(``)).length + cjChars.length * 0.56 + + timeToRead = Math.round(wordCount / avgWPM) + if (timeToRead === 0) { + timeToRead = 1 + } + return timeToRead +}