From aebe2137b0221f2c40391a4076b56f69b6d81c36 Mon Sep 17 00:00:00 2001 From: Sivan Date: Fri, 21 Feb 2020 01:49:22 +0800 Subject: [PATCH] feat: support CJK & ANS spacing (#12) --- .github/workflows/deploy.yml | 33 +- .github/workflows/nodejs.yml | 31 ++ .npmignore | 21 - README.md | 13 +- {demo => _site}/favicon.png | Bin {demo => _site}/favicon.svg | 0 _site/heti-addon.js | 753 ++++++++++++++++++++++++++++++++++ {dist => _site}/heti.css | 59 ++- {demo => _site}/index.css | 13 +- {demo => _site}/index.html | 153 ++++--- {demo => _site}/normalize.css | 0 {demo => _site}/orange.jpg | Bin add-ons/add-on.js | 107 +++++ demo/heti.min.css | 5 - dist/heti-addon.min.js | 1 + dist/heti.min.css | 2 +- lib/_heading.scss | 15 +- lib/_inline.scss | 27 +- lib/helpers/_add-on.scss | 23 ++ lib/helpers/_block.scss | 7 + lib/helpers/_inline.scss | 20 +- lib/heti.scss | 3 + package-lock.json | 395 +++++++++--------- package.json | 44 +- rollup.config.js | 30 ++ 25 files changed, 1420 insertions(+), 335 deletions(-) create mode 100644 .github/workflows/nodejs.yml delete mode 100644 .npmignore rename {demo => _site}/favicon.png (100%) rename {demo => _site}/favicon.svg (100%) create mode 100644 _site/heti-addon.js rename {dist => _site}/heti.css (94%) rename {demo => _site}/index.css (93%) rename {demo => _site}/index.html (81%) rename {demo => _site}/normalize.css (100%) rename {demo => _site}/orange.jpg (100%) create mode 100644 add-ons/add-on.js delete mode 100644 demo/heti.min.css create mode 100644 dist/heti-addon.min.js create mode 100644 lib/helpers/_add-on.scss create mode 100644 rollup.config.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec04246..ab26d46 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,13 +9,26 @@ jobs: build-and-deploy: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@master - - - name: Deploy to GitHub Pages - uses: Cecilapp/GitHub-Pages-deploy@master - env: - EMAIL: sun.sivan@gmail.com - GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} - BASE_BRANCH: master - BUILD_DIR: demo + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 10 + - name: NPM Install and Test + run: | + npm install + npm run test + npm run build + - name: Publish + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_TOKEN }} + - name: Deploy to gh-pages + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + BRANCH: gh-pages + FOLDER: _site diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..a859d57 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,31 @@ +--- + +name: Node CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-18.04 + + strategy: + matrix: + node-version: [10.x, 12.x] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: npm install and test + run: | + npm ci + npm test + env: + CI: true + - name: npm build + run: npm run build + env: + CI: true diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 6b34dda..0000000 --- a/.npmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Editor generate files -.idea/ -.settings/ - -# Dev dependencies and cache files -demo/ -node_modules/ -npm-debug.log - -# Folder view configuration files -.DS_Store -Desktop.ini - -# Thumbnail cache files -*~ -._* -Thumbs.db - -# Files that might appear on external disks -.Spotlight-V100 -.Trashes diff --git a/README.md b/README.md index 2d7e1d9..698b855 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,15 @@ 预览:[https://sivan.github.io/heti/](https://sivan.github.io/heti/) 主要特性: -- 全标签样式统一; - 贴合网格的排版; -- 预置简体/繁体中文多种预设字体族(仅限桌面端); -- 预置横排、直排(竖排)样式; +- 全标签样式美化; - 预置古文、诗词样式; -- 预置行间注排版样式; -- 预置多栏排版样式; +- 预置多种排版样式(行间注、多栏、竖排等); +- 多种预设字体族(仅限桌面端); +- 简/繁体中文支持; +- 中西文混排美化(基于JavaScript脚本实现); - 兼容 *normalize.css*、*CSS Reset* 等常见样式重置; - 移动端支持; -- 基于 BEM; - …… 总之,用上就会变好看。 @@ -37,9 +36,9 @@ ## WIP -- [ ] 中、西文混排 - [ ] 标点挤压 - [ ] 标点悬挂 +- [x] 中、西文混排 - [x] 繁体中文支持 - [x] 诗词版式 - [x] 行间注版式 diff --git a/demo/favicon.png b/_site/favicon.png similarity index 100% rename from demo/favicon.png rename to _site/favicon.png diff --git a/demo/favicon.svg b/_site/favicon.svg similarity index 100% rename from demo/favicon.svg rename to _site/favicon.svg diff --git a/_site/heti-addon.js b/_site/heti-addon.js new file mode 100644 index 0000000..6aad703 --- /dev/null +++ b/_site/heti-addon.js @@ -0,0 +1,753 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Heti = factory()); +}(this, (function () { 'use strict'; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var findAndReplaceDOMText = createCommonjsModule(function (module) { + /** + * findAndReplaceDOMText v 0.4.6 + * @author James Padolsey http://james.padolsey.com + * @license http://unlicense.org/UNLICENSE + * + * Matches the text of a DOM node against a regular expression + * and replaces each match (or node-separated portions of the match) + * in the specified element. + */ + (function (root, factory) { + if ( module.exports) { + // Node/CommonJS + module.exports = factory(); + } else { + // Browser globals + root.findAndReplaceDOMText = factory(); + } + }(commonjsGlobal, function factory() { + + var PORTION_MODE_RETAIN = 'retain'; + var PORTION_MODE_FIRST = 'first'; + + var doc = document; + var hasOwn = {}.hasOwnProperty; + + function escapeRegExp(s) { + return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); + } + + function exposed() { + // Try deprecated arg signature first: + return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments); + } + + function deprecated(regex, node, replacement, captureGroup, elFilter) { + if ((node && !node.nodeType) && arguments.length <= 2) { + return false; + } + var isReplacementFunction = typeof replacement == 'function'; + + if (isReplacementFunction) { + replacement = (function(original) { + return function(portion, match) { + return original(portion.text, match.startIndex); + }; + }(replacement)); + } + + // Awkward support for deprecated argument signature (<0.4.0) + var instance = findAndReplaceDOMText(node, { + + find: regex, + + wrap: isReplacementFunction ? null : replacement, + replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'), + + prepMatch: function(m, mi) { + + // Support captureGroup (a deprecated feature) + + if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches'; + + if (captureGroup > 0) { + var cg = m[captureGroup]; + m.index += m[0].indexOf(cg); + m[0] = cg; + } + + m.endIndex = m.index + m[0].length; + m.startIndex = m.index; + m.index = mi; + + return m; + }, + filterElements: elFilter + }); + + exposed.revert = function() { + return instance.revert(); + }; + + return true; + } + + /** + * findAndReplaceDOMText + * + * Locates matches and replaces with replacementNode + * + * @param {Node} node Element or Text node to search within + * @param {RegExp} options.find The regular expression to match + * @param {String|Element} [options.wrap] A NodeName, or a Node to clone + * @param {String} [options.wrapClass] A classname to append to the wrapping element + * @param {String|Function} [options.replace='$&'] What to replace each match with + * @param {Function} [options.filterElements] A Function to be called to check whether to + * process an element. (returning true = process element, + * returning false = avoid element) + */ + function findAndReplaceDOMText(node, options) { + return new Finder(node, options); + } + + exposed.NON_PROSE_ELEMENTS = { + br:1, hr:1, + // Media / Source elements: + script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1, + // Input elements + input:1, textarea:1, select:1, option:1, optgroup: 1, button:1 + }; + + exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = { + + // Elements that will not contain prose or block elements where we don't + // want prose to be matches across element borders: + + // Block Elements + address:1, article:1, aside:1, blockquote:1, dd:1, div:1, + dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1, + h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1, + output:1, p:1, pre:1, section:1, ul:1, + // Other misc. elements that are not part of continuous inline prose: + br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1, + // Media / Source elements: + script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1, + // Input elements + input:1, textarea:1, select:1, option:1, optgroup:1, button:1, + // Table related elements: + table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1 + + }; + + exposed.NON_INLINE_PROSE = function(el) { + return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase()); + }; + + // Presets accessed via `options.preset` when calling findAndReplaceDOMText(): + exposed.PRESETS = { + prose: { + forceContext: exposed.NON_INLINE_PROSE, + filterElements: function(el) { + return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase()); + } + } + }; + + exposed.Finder = Finder; + + /** + * Finder -- encapsulates logic to find and replace. + */ + function Finder(node, options) { + + var preset = options.preset && exposed.PRESETS[options.preset]; + + options.portionMode = options.portionMode || PORTION_MODE_RETAIN; + + if (preset) { + for (var i in preset) { + if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) { + options[i] = preset[i]; + } + } + } + + this.node = node; + this.options = options; + + // Enable match-preparation method to be passed as option: + this.prepMatch = options.prepMatch || this.prepMatch; + + this.reverts = []; + + this.matches = this.search(); + + if (this.matches.length) { + this.processMatches(); + } + + } + + Finder.prototype = { + + /** + * Searches for all matches that comply with the instance's 'match' option + */ + search: function() { + + var match; + var matchIndex = 0; + var offset = 0; + var regex = this.options.find; + var textAggregation = this.getAggregateText(); + var matches = []; + var self = this; + + regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex; + + matchAggregation(textAggregation); + + function matchAggregation(textAggregation) { + for (var i = 0, l = textAggregation.length; i < l; ++i) { + + var text = textAggregation[i]; + + if (typeof text !== 'string') { + // Deal with nested contexts: (recursive) + matchAggregation(text); + continue; + } + + if (regex.global) { + while (match = regex.exec(text)) { + matches.push(self.prepMatch(match, matchIndex++, offset)); + } + } else { + if (match = text.match(regex)) { + matches.push(self.prepMatch(match, 0, offset)); + } + } + + offset += text.length; + } + } + + return matches; + + }, + + /** + * Prepares a single match with useful meta info: + */ + prepMatch: function(match, matchIndex, characterOffset) { + + if (!match[0]) { + throw new Error('findAndReplaceDOMText cannot handle zero-length matches'); + } + + match.endIndex = characterOffset + match.index + match[0].length; + match.startIndex = characterOffset + match.index; + match.index = matchIndex; + + return match; + }, + + /** + * Gets aggregate text within subject node + */ + getAggregateText: function() { + + var elementFilter = this.options.filterElements; + var forceContext = this.options.forceContext; + + return getText(this.node); + + /** + * Gets aggregate text of a node without resorting + * to broken innerText/textContent + */ + function getText(node) { + + if (node.nodeType === Node.TEXT_NODE) { + return [node.data]; + } + + if (elementFilter && !elementFilter(node)) { + return []; + } + + var txt = ['']; + var i = 0; + + if (node = node.firstChild) do { + + if (node.nodeType === Node.TEXT_NODE) { + txt[i] += node.data; + continue; + } + + var innerText = getText(node); + + if ( + forceContext && + node.nodeType === Node.ELEMENT_NODE && + (forceContext === true || forceContext(node)) + ) { + txt[++i] = innerText; + txt[++i] = ''; + } else { + if (typeof innerText[0] === 'string') { + // Bridge nested text-node data so that they're + // not considered their own contexts: + // I.e. ['some', ['thing']] -> ['something'] + txt[i] += innerText.shift(); + } + if (innerText.length) { + txt[++i] = innerText; + txt[++i] = ''; + } + } + } while (node = node.nextSibling); + + return txt; + + } + + }, + + /** + * Steps through the target node, looking for matches, and + * calling replaceFn when a match is found. + */ + processMatches: function() { + + var matches = this.matches; + var node = this.node; + var elementFilter = this.options.filterElements; + + var startPortion, + endPortion, + innerPortions = [], + curNode = node, + match = matches.shift(), + atIndex = 0, // i.e. nodeAtIndex + portionIndex = 0, + doAvoidNode, + nodeStack = [node]; + + out: while (true) { + + if (curNode.nodeType === Node.TEXT_NODE) { + + if (!endPortion && curNode.length + atIndex >= match.endIndex) { + // We've found the ending + // (Note that, in the case of a single portion, it'll be an + // endPortion, not a startPortion.) + endPortion = { + node: curNode, + index: portionIndex++, + text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex), + + // If it's the first match (atIndex==0) we should just return 0 + indexInMatch: atIndex === 0 ? 0 : atIndex - match.startIndex, + + indexInNode: match.startIndex - atIndex, + endIndexInNode: match.endIndex - atIndex, + isEnd: true + }; + + } else if (startPortion) { + // Intersecting node + innerPortions.push({ + node: curNode, + index: portionIndex++, + text: curNode.data, + indexInMatch: atIndex - match.startIndex, + indexInNode: 0 // always zero for inner-portions + }); + } + + if (!startPortion && curNode.length + atIndex > match.startIndex) { + // We've found the match start + startPortion = { + node: curNode, + index: portionIndex++, + indexInMatch: 0, + indexInNode: match.startIndex - atIndex, + endIndexInNode: match.endIndex - atIndex, + text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex) + }; + } + + atIndex += curNode.data.length; + + } + + doAvoidNode = curNode.nodeType === Node.ELEMENT_NODE && elementFilter && !elementFilter(curNode); + + if (startPortion && endPortion) { + + curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion); + + // processMatches has to return the node that replaced the endNode + // and then we step back so we can continue from the end of the + // match: + + atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode); + + startPortion = null; + endPortion = null; + innerPortions = []; + match = matches.shift(); + portionIndex = 0; + + if (!match) { + break; // no more matches + } + + } else if ( + !doAvoidNode && + (curNode.firstChild || curNode.nextSibling) + ) { + // Move down or forward: + if (curNode.firstChild) { + nodeStack.push(curNode); + curNode = curNode.firstChild; + } else { + curNode = curNode.nextSibling; + } + continue; + } + + // Move forward or up: + while (true) { + if (curNode.nextSibling) { + curNode = curNode.nextSibling; + break; + } + curNode = nodeStack.pop(); + if (curNode === node) { + break out; + } + } + + } + + }, + + /** + * Reverts ... TODO + */ + revert: function() { + // Reversion occurs backwards so as to avoid nodes subsequently + // replaced during the matching phase (a forward process): + for (var l = this.reverts.length; l--;) { + this.reverts[l](); + } + this.reverts = []; + }, + + prepareReplacementString: function(string, portion, match) { + var portionMode = this.options.portionMode; + if ( + portionMode === PORTION_MODE_FIRST && + portion.indexInMatch > 0 + ) { + return ''; + } + string = string.replace(/\$(\d+|&|`|')/g, function($0, t) { + var replacement; + switch(t) { + case '&': + replacement = match[0]; + break; + case '`': + replacement = match.input.substring(0, match.startIndex); + break; + case '\'': + replacement = match.input.substring(match.endIndex); + break; + default: + replacement = match[+t] || ''; + } + return replacement; + }); + + if (portionMode === PORTION_MODE_FIRST) { + return string; + } + + if (portion.isEnd) { + return string.substring(portion.indexInMatch); + } + + return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length); + }, + + getPortionReplacementNode: function(portion, match) { + + var replacement = this.options.replace || '$&'; + var wrapper = this.options.wrap; + var wrapperClass = this.options.wrapClass; + + if (wrapper && wrapper.nodeType) { + // Wrapper has been provided as a stencil-node for us to clone: + var clone = doc.createElement('div'); + clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper); + wrapper = clone.firstChild; + } + + if (typeof replacement == 'function') { + replacement = replacement(portion, match); + if (replacement && replacement.nodeType) { + return replacement; + } + return doc.createTextNode(String(replacement)); + } + + var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper; + + if (el && wrapperClass) { + el.className = wrapperClass; + } + + replacement = doc.createTextNode( + this.prepareReplacementString( + replacement, portion, match + ) + ); + + if (!replacement.data) { + return replacement; + } + + if (!el) { + return replacement; + } + + el.appendChild(replacement); + + return el; + }, + + replaceMatch: function(match, startPortion, innerPortions, endPortion) { + + var matchStartNode = startPortion.node; + var matchEndNode = endPortion.node; + + var precedingTextNode; + var followingTextNode; + + if (matchStartNode === matchEndNode) { + + var node = matchStartNode; + + if (startPortion.indexInNode > 0) { + // Add `before` text node (before the match) + precedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode)); + node.parentNode.insertBefore(precedingTextNode, node); + } + + // Create the replacement node: + var newNode = this.getPortionReplacementNode( + endPortion, + match + ); + + node.parentNode.insertBefore(newNode, node); + + if (endPortion.endIndexInNode < node.length) { // ????? + // Add `after` text node (after the match) + followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode)); + node.parentNode.insertBefore(followingTextNode, node); + } + + node.parentNode.removeChild(node); + + this.reverts.push(function() { + if (precedingTextNode === newNode.previousSibling) { + precedingTextNode.parentNode.removeChild(precedingTextNode); + } + if (followingTextNode === newNode.nextSibling) { + followingTextNode.parentNode.removeChild(followingTextNode); + } + newNode.parentNode.replaceChild(node, newNode); + }); + + return newNode; + + } else { + // Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order) + + + precedingTextNode = doc.createTextNode( + matchStartNode.data.substring(0, startPortion.indexInNode) + ); + + followingTextNode = doc.createTextNode( + matchEndNode.data.substring(endPortion.endIndexInNode) + ); + + var firstNode = this.getPortionReplacementNode( + startPortion, + match + ); + + var innerNodes = []; + + for (var i = 0, l = innerPortions.length; i < l; ++i) { + var portion = innerPortions[i]; + var innerNode = this.getPortionReplacementNode( + portion, + match + ); + portion.node.parentNode.replaceChild(innerNode, portion.node); + this.reverts.push((function(portion, innerNode) { + return function() { + innerNode.parentNode.replaceChild(portion.node, innerNode); + }; + }(portion, innerNode))); + innerNodes.push(innerNode); + } + + var lastNode = this.getPortionReplacementNode( + endPortion, + match + ); + + matchStartNode.parentNode.insertBefore(precedingTextNode, matchStartNode); + matchStartNode.parentNode.insertBefore(firstNode, matchStartNode); + matchStartNode.parentNode.removeChild(matchStartNode); + + matchEndNode.parentNode.insertBefore(lastNode, matchEndNode); + matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode); + matchEndNode.parentNode.removeChild(matchEndNode); + + this.reverts.push(function() { + precedingTextNode.parentNode.removeChild(precedingTextNode); + firstNode.parentNode.replaceChild(matchStartNode, firstNode); + followingTextNode.parentNode.removeChild(followingTextNode); + lastNode.parentNode.replaceChild(matchEndNode, lastNode); + }); + + return lastNode; + } + } + + }; + + return exposed; + + })); + }); + + /** + * Heti add-on v 0.1.0 + * Add right spacing between CJK & ANS characters + */ + + // 正则表达式来自 pangu.js https://github.com/vinta/pangu.js + const CJK = '\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff'; + const A = 'A-Za-z\u0370-\u03ff'; + const N = '0-9'; + const S = '`~!@#\\$%\\^&\\*\\(\\)-_=\\+\\[\\]{}\\\\\\|;:\'",<.>\\/\\?'; + const ANS = `${A}${N}${S}`; + const HETI_NON_CONTIGUOUS_ELEMENTS = { + // Block Elements + address: 1, article: 1, aside: 1, blockquote: 1, dd: 1, div: 1, + dl: 1, fieldset: 1, figcaption: 1, figure: 1, footer: 1, form: 1, h1: 1, h2: 1, h3: 1, + h4: 1, h5: 1, h6: 1, header: 1, hgroup: 1, hr: 1, main: 1, nav: 1, noscript: 1, ol: 1, + output: 1, p: 1, pre: 1, section: 1, ul: 1, + // Other misc. elements that are not part of continuous inline prose: + br: 1, li: 1, summary: 1, dt: 1, details: 1, rp: 1, rt: 1, rtc: 1, + // Media / Source elements: + script: 1, style: 1, img: 1, video: 1, audio: 1, canvas: 1, svg: 1, map: 1, object: 1, + // Input elements + input: 1, textarea: 1, select: 1, option: 1, optgroup: 1, button: 1, + // Table related elements: + table: 1, tbody: 1, thead: 1, th: 1, tr: 1, td: 1, caption: 1, col: 1, tfoot: 1, colgroup: 1, + // Inline elements + ins: 1, del: 1, s: 1, + }; + const HETI_SKIPPED_ELEMENTS = { + br: 1, hr: 1, + // Media / Source elements: + script: 1, style: 1, img: 1, video: 1, audio: 1, canvas: 1, svg: 1, map: 1, object: 1, + // Input elements: + input: 1, textarea: 1, select: 1, option: 1, optgroup: 1, button: 1, + // Pre elements: + pre: 1, code: 1, sup: 1, sub: 1, + // Heti elements + 'heti-spacing': 1, + }; + const HETI_SKIPPED_CLASS = 'heti-skip'; + const hasOwn = {}.hasOwnProperty; + + class Heti { + constructor (rootSelector) { + this.rootSelector = rootSelector || '.heti'; + this.REG_FULL = new RegExp(`(?<=[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)* *)(?=[${CJK}])`, 'g'); + this.REG_START = new RegExp(`([${ANS}]+(?: +[${ANS}]+)* *)(?=[${CJK}])`, 'g'); + this.REG_END = new RegExp(`(?<=[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)*)`, 'g'); + this.funcForceContext = function forceContext (el) { + return hasOwn.call(HETI_NON_CONTIGUOUS_ELEMENTS, el.nodeName.toLowerCase()) + }; + this.funcFilterElements = function filterElements (el) { + return ( + !(el.classList && el.classList.contains(HETI_SKIPPED_CLASS)) && + !hasOwn.call(HETI_SKIPPED_ELEMENTS, el.nodeName.toLowerCase()) + ) + }; + } + + spacingElements (elmList) { + for (let $$root of elmList) { + this.spacingElement($$root); + } + } + + spacingElement ($$elm) { + const commonConfig = { + forceContext: this.funcForceContext, + filterElements: this.funcFilterElements, + }; + const getWrapper = function (classList, text) { + const $$r = document.createElement('heti-spacing'); + $$r.className = classList; + $$r.textContent = text.trim(); + return $$r + }; + + findAndReplaceDOMText($$elm, Object.assign(commonConfig, { + find: this.REG_FULL, + replace: portion => getWrapper('heti-spacing-start heti-spacing-end', portion.text), + })); + + findAndReplaceDOMText($$elm, Object.assign(commonConfig, { + find: this.REG_START, + replace: portion => getWrapper('heti-spacing-start', portion.text), + })); + + findAndReplaceDOMText($$elm, Object.assign(commonConfig, { + find: this.REG_END, + replace: portion => getWrapper('heti-spacing-end', portion.text), + })); + } + + autoSpacing () { + document.addEventListener('DOMContentLoaded', () => { + const $$rootList = document.querySelectorAll(this.rootSelector); + + for (let $$root of $$rootList) { + this.spacingElement($$root); + } + }); + } + } + + return Heti; + +}))); diff --git a/dist/heti.css b/_site/heti.css similarity index 94% rename from dist/heti.css rename to _site/heti.css index ba9382a..7a6d9cc 100644 --- a/dist/heti.css +++ b/_site/heti.css @@ -71,6 +71,7 @@ max-width: 42em; font-size: 16px; font-weight: 400; + -webkit-font-smoothing: subpixel-antialiased; line-height: 1.5; } @@ -182,19 +183,16 @@ margin-block-end: 24px; font-size: 32px; line-height: 48px; - letter-spacing: 1.6px; } .heti h2 { font-size: 24px; line-height: 36px; - letter-spacing: 1.2px; } .heti h3 { font-size: 20px; line-height: 36px; - letter-spacing: 1px; } .heti h4 { @@ -212,6 +210,20 @@ line-height: 24px; } +.heti h1, +.heti h2, +.heti h3 { + letter-spacing: 0.05em; +} + +.heti h1:not(:lang(zh)):not(:lang(ja)):not(:lang(kr)), .heti h1:not(:lang(zh)), +.heti h2:not(:lang(zh)):not(:lang(ja)):not(:lang(kr)), +.heti h2:not(:lang(zh)), +.heti h3:not(:lang(zh)):not(:lang(ja)):not(:lang(kr)), +.heti h3:not(:lang(zh)) { + letter-spacing: 0; +} + .heti h1 + h2, .heti h2 + h3, .heti h3 + h4, @@ -373,7 +385,7 @@ .heti rt { font-size: 0.875em; - color: rgba(0, 0, 0, 0.88); + font-weight: 400; } .heti small { @@ -391,6 +403,7 @@ margin-inline-end: 0.25em; font-size: 0.75em; font-family: "Helvetica Neue", Helvetica, Arial, "Heti Hei", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-style: normal; line-height: 1; vertical-align: baseline; } @@ -403,6 +416,11 @@ top: -0.5em; } +.heti sup:target, +.heti sup a:target { + background-color: #def; +} + .heti summary { padding-inline-start: 1em; outline: 0; @@ -685,6 +703,7 @@ .heti .heti-x-large { font-size: 20px; line-height: 30px; + letter-spacing: 0.05em; } .heti .heti-small { @@ -734,6 +753,10 @@ margin-block-end: 0; } +.heti .heti-fn li:target { + background-color: #def; +} + .heti .heti-hang { position: absolute; line-height: inherit; @@ -748,3 +771,31 @@ .heti .heti-em:not(:lang(zh)):not(:lang(ja)):not(:lang(kr)), .heti .heti-em:not(:lang(zh)) { -webkit-text-emphasis: none; } + +.heti .heti-ruby--inline { + display: inline-flex; + flex-direction: column-reverse; + height: 1.5em; +} + +.heti .heti-ruby--inline rt { + display: inline; + line-height: 0.5; +} + +.heti heti-spacing { + display: inline; +} + +.heti heti-spacing + sup, +.heti heti-spacing + sub { + margin-inline-start: 0; +} + +.heti .heti-spacing-start { + margin-inline-end: 0.25em; +} + +.heti .heti-spacing-end { + margin-inline-start: 0.25em; +} diff --git a/demo/index.css b/_site/index.css similarity index 93% rename from demo/index.css rename to _site/index.css index d723adf..8547980 100644 --- a/demo/index.css +++ b/_site/index.css @@ -50,8 +50,9 @@ a { .article[data-bg-grid="grid-24"] { background-size: 100% 24px; - background-image: linear-gradient(to right, rgba(255, 255, 255, 0) 31px, #eee 1px, rgba(255, 255, 255, 0) 33px), - linear-gradient(rgba(255, 255, 255, 0) 23px, #eee 1px); + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0) 31px, #eee 1px, rgba(255, 255, 255, 0) 33px), + linear-gradient(rgba(255, 255, 255, 0) 23px, #eee 1px); outline-color: #eee; } @@ -63,8 +64,9 @@ a { .article[data-bg-grid="grid-12"] { background-size: 100% 12px; - background-image: linear-gradient(to right, rgba(255, 255, 255, 0) 31px, #eee 1px, rgba(255, 255, 255, 0) 33px), - linear-gradient(rgba(255, 255, 255, 0) 11px, #eee 12px); + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0) 31px, #eee 1px, rgba(255, 255, 255, 0) 33px), + linear-gradient(rgba(255, 255, 255, 0) 11px, #eee 12px); outline-color: #eee; } @@ -313,4 +315,7 @@ a { margin-block-start: 12px; margin-block-end: 0; } + .article .article__toc ol ol { + margin-block-start: 0; + } } diff --git a/demo/index.html b/_site/index.html similarity index 81% rename from demo/index.html rename to _site/index.html index bffd2e6..7f35bf7 100644 --- a/demo/index.html +++ b/_site/index.html @@ -6,7 +6,7 @@ - + @@ -15,7 +15,7 @@

赫蹏

古代称用以书写的小幅绢帛。后亦以借指纸。《汉书·外戚传下·孝成赵皇后》:武(籍武 )发篋中,有裹药二枚,赫蹏书。颜师古注:邓展曰:赫音兄弟鬩墙之鬩。应劭曰:赫蹏,薄小纸也。赵彦卫 《云麓漫钞》卷七:《赵后传》所谓『赫蹏』者,注云薄小纸,然其寔亦縑帛。
-