diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0771922
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+/dist
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..261d4bc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,134 @@
+# TextFrame
+一个通过canvas将长文本分成若干页渲染的js工具,处理排版禁则,支持辅助平面Unicode字符。目前仅工作于浏览器环境。
+
+## 项目介绍
+在浏览器中,尽管css有分栏布局,但却很难实现“分页”。浏览器没有提供计算一个盒子可以显示多少文本的API,只有Canvas的上下文提供了一个`measureText()`方法,但它只能简单的计算一段文本的渲染宽度,当文本不止一行时就无法计算了。由于不同字体不同字符有不同的宽度,再加上各浏览器对排版禁则的处理不一致,基本杜绝了计算浏览器原生DOM元素能够显示多少文本的可能。
+
+于是我参考W3C对中文排版的[草案](https://www.w3.org/TR/2020/WD-clreq-20201001/#prohibition_rules_for_line_start_end)中的排版禁则,通过[CanvasRenderingContext2D.measureText()](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/measureText)方法计算字符宽度,对文本进行分页。
+
+### 行首禁则
+| 名称 | 符号 |
+| ---- | ------ |
+| 点号 | 、,,..。::;;!!?? |
+| 结束引号 | '"」』”’ |
+| 结束括号 | )]})】〗〕]} |
+| 结束乙式书名号 | 》〉 |
+| 连接号 | –~~— |
+| 间隔号 | ·.‧•・ |
+| 分隔号 | // |
+
+### 行尾禁则
+| 名称 | 符号 |
+| ---- | ------ |
+| 开始引号 | 「『“‘ |
+| 开始括号 | ([{(【〖〔[〔 |
+| 开始单双书名号 | 《〈 |
+| 分隔号 | // |
+
+### 符号分离禁则
+| 名称 | 符号 |
+| ---- | ------ |
+| 破折号 | ── |
+| 省略号 | …… |
+
+本项目默认按照上述禁则处理排版禁则,但可以通过[选项](https://mattuylee.github.io/text-frame/zh/options.md)控制排版禁则。
+草案中规定,当碰到行首为(行首禁则中的)标点时,应遵守「先挤进,后推出」原则,即先尝试压缩当前行的标点,无法挤压再取前一行的最后一个字至下一行。考虑到标点符号压缩的复杂性,本项目暂未实现该特性,而是直接尝试取上一行的最后一字到下一行,如果直到上一行首都没找到允许出现在行首的字符,则采取不处理的方式。
+
+## 功能
+本项目定义了文本片段(text fragment)的概念。通过在选项中指定一个文本片段数组,可以对每一个片段设置不同的样式,如字体、颜色、对齐方式等,具体请参考[配置项](https://mattuylee.github.io/text-frame/zh/options.md)。一般来说,一个文本片段即一个文本段落,虽然文本片段内也支持换行,但不会有段间距和行缩进。在本项目文档中文本片段和文本段落等价,都是指`FragmentOptions`定义的文本片段。
+
+本项目支持以下特性:
+* 每个段落单独设置字体、缩进、边距、字体颜色等;
+* 支持两端对齐;
+* 支持自定义排版禁则;
+* 可配置段落之间边距是否折叠;
+* 支持rtl模式;
+
+这是一个[在线例子](https://mattuylee.github.io/text-frame/zh/example.html)。
+
+## 使用
+本项目仅支持浏览器环境,且依赖于Canvas 2D上下文,请参考[canvas兼容性](https://caniuse.com/?search=canvas)。
+
+### API
+```typescript
+// 计算分页
+function computeTextFrames(options: FrameOptions): TextFrame[];
+// 渲染分页到canvas
+function renderFrame(context: CanvasRenderingContext2D, frame: TextFrame, clear: boolean): void;
+```
+
+### npm
+`npm install @mattuy/text-frame --save`
+### ES2015 & CommonJS
+```javascript
+// cjs
+// const { computeTextFrames, renderFrame } = require('@mattuy/text-frame');
+// or esm
+import { computeTextFrames, renderFrame } from '@mattuy/text-frame/esm';
+
+const frames = computeTextFrames({
+ viewWidth: 320,
+ viewHeight: 640,
+ fontSize: 16,
+ margin: 8,
+ color: '#000',
+ fragments: [
+ {
+ color: 'green',
+ fontSize: 20,
+ margin: 32,
+ textAlign: 'center',
+ text: "标题"
+ },
+ {
+ textIndent: 32,
+ fontFamily: 'serif',
+ margin: { bottom: 32 },
+ marginCollapse: true,
+ textAlign: 'justify',
+ text: "这是一个多行文本。"
+ }
+ ]
+});
+const canvas = document.createElement('canvas');
+canvas.style.width = '320px';
+canvas.style.height = '640px';
+document.body.append(canvas);
+renderFrame(canvas.getContext('2d'), frames[0]);
+```
+
+### 全局引入
+```html
+
+
+```
+
+配置说明请参考[配置项](https://mattuylee.github.io/text-frame/zh/options.md)。
+
+
+## 源码编译
+* 克隆仓库
+`git clone https://github.com/mattuylee/text-frame.git`
+
+* 安装依赖
+`cd text-frame`
+`npm install`
+
+### 发布
+`npm run dist`
+
+### 启动demo
+`npm run example`
+
+### 调试代码
+执行`npm run debug`,然后rollup会监听src下文件的变化自动重新编译。可浏览器直接打开`example/index.html`测试。
+
+
+## Licence
+MIT
\ No newline at end of file
diff --git a/docs/en/README-EN.md b/docs/en/README-EN.md
new file mode 100644
index 0000000..4f4ee2c
--- /dev/null
+++ b/docs/en/README-EN.md
@@ -0,0 +1,124 @@
+# TextFrame
+
+[中文版](https://github.com/mattuylee/text-frame) | [English](https://github.com/mattuylee/text-frame/docs/en/README-EN.md)
+
+A javascript tool to split long text into frames, with typesetting prohibition processed and unicode full support.
+
+## Introduction
+It's difficute to compute how many characters a DOM element can show, because diffrent User Agents have diffrent typesetting rules. So for that, we have to draw text on a canvas with our own rules to know how to split text into several text frames, since we can compute character width through [CanvasRenderingContext2D.measureText()](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/measureText).
+
+refering to [W3C draft](https://www.w3.org/TR/2020/WD-clreq-20201001/#prohibition_rules_for_line_start_end) for Chinese typesetting, following are my rules:
+
+### Prohibition Rules for Line Start
+| Punctuation Name | The Punctuation Marks |
+| ---- | ------ |
+| Pause or Stop | 、,,..。::;;!!?? |
+| Closing Quotation | '"」』”’ |
+| Closing Parentheses | )]})】〗〕]} |
+| Closing Angle Brackets | 》〉 |
+| Connectors | –~~— |
+| Interpuncts | ·.‧•・ |
+| Solidi | // |
+
+### Prohibition Rules for Line End
+| Punctuation Name | The Punctuation Marks |
+| ---- | ------ |
+| Opening Quotation | 「『“‘ |
+| opening Parentheses | ([{(【〖〔[〔 |
+| Opening Angle Brackets | 《〈 |
+| Solidi | // |
+
+### Prohibition Rules for Unbreakable Marks
+| Name | Mark |
+| ---- | ------ |
+| Em dash and long dash | ── |
+| Ellipsis | …… |
+
+This library take the above as default rules, but it can be configued with [options](https://mattuylee.github.io/text-frame/en/options.md).
+
+Here is an online [demo](https://mattuylee.github.io/text-frame/en/example.html).
+
+## Usage
+Browser environment is required, and [canvas support](https://caniuse.com/?search=canvas) is needed. This library CANNOT work under web worker.
+
+### API
+```typescript
+// compute text frames
+function computeTextFrames(options: FrameOptions): TextFrame[];
+// render text frame
+function renderFrame(context: CanvasRenderingContext2D, frame: TextFrame, clear: boolean): void;
+```
+
+### npm
+`npm install @mattuy/text-frame --save`
+### ES2015 & CommonJS
+```javascript
+// cjs
+// const { computeTextFrames, renderFrame } = require('@mattuy/text-frame');
+// or esm
+import { computeTextFrames, renderFrame } from '@mattuy/text-frame/esm';
+
+const frames = computeTextFrames({
+ viewWidth: 320,
+ viewHeight: 640,
+ fontSize: 16,
+ margin: 8,
+ color: '#000',
+ fragments: [
+ {
+ color: 'green',
+ fontSize: 20,
+ margin: 32,
+ textAlign: 'center',
+ text: "Caption"
+ },
+ {
+ textIndent: 32,
+ fontFamily: 'serif',
+ margin: { bottom: 32 },
+ marginCollapse: true,
+ textAlign: 'justify',
+ text: "This is a multi-line text. Pass your text paragraph as this."
+ }
+ ]
+});
+const canvas = document.createElement('canvas');
+canvas.style.width = '320px';
+canvas.style.height = '640px';
+document.body.append(canvas);
+renderFrame(canvas.getContext('2d'), frames[0], true);
+```
+
+### Globally Script
+```html
+
+
+```
+Option reference sits [here](https://mattuylee.github.io/text-frame/en/options.md).
+
+
+## Build from Source
+* clone the repo
+`git clone https://github.com/mattuylee/text-frame.git`
+
+* install dependences
+`cd text-frame`
+`npm install`
+
+### Distribution
+`npm run dist`
+
+### Start a demo serve
+`npm run example`
+
+### Debug
+Run `npm run debug`, then open `example/index.html` in your browser, rollup will watch your code changes and automatically rebuild.
+
+## Licence
+MIT
\ No newline at end of file
diff --git a/docs/en/options.md b/docs/en/options.md
new file mode 100644
index 0000000..59f9cb4
--- /dev/null
+++ b/docs/en/options.md
@@ -0,0 +1,130 @@
+# Options
+
+## API Signature
+```typescript
+export function computeTextFrames(options: FrameOptions): TextFrame[];
+export function renderFrame(context: CanvasRenderingContext2D, frame: TextFrame, clear: boolean): void;
+```
+
+## FrameOptions
+### viewWidth
+* Type: `number`
+* Default: `300`
+* Description: view width, must equal to canvas.style.width, but be numeric
+
+### viewHeight
+* Type: `number`
+* Default: `150`
+* Description: view height
+
+### canvasWidth
+* Type: `number`
+* Default: compute with `viewWidth`
+* Description: canvas.width,default is `viewWidth * window.devicePixelRatio`
+
+### canvasHeight
+* Type: `number`
+* Default: 根据`viewHeight`计算。
+* Description: canvas.height is `viewHeight * window.devicePixelRatio`
+
+### margin
+* Type: `number | { left?: number, right?: number, top?: number, bottom?: number }`
+* Default: `0`
+* Description: margin for **frame**
+
+### lineStartProhibitedMarks
+* Type: `string`
+* Default: `、,,..。::;;!!??'"」』”’)]})】〗〕]}》〉–~~—·.‧•・//`
+* Description: prohibition characters for line start
+
+### lineEndProhibitedMarks
+* Type: `string`
+* Default: `「『“‘([{(【〖〔[〔《〈//`
+* Description: prohibition characters for line end
+
+### unbreakableRule
+* Type: `RegExp`
+* Default: `/──|……|[\w\d]+/`
+* Description: unbreakable marks. If not empty, must be a RegExp instance. Marks match the rule will not be split into diffrent lines
+
+### fragments
+* Type: `FragmentOptions[]`
+* Default: `null`
+* Description: text fragments, typically one paragraph one fragment
+
+
+## FrameOptions & FragmentOptions
+Options for both frame and fragment. Fragment option will inhirit from frame option if it's empty except `margin`.
+
+### fontFamily
+* Type: `string`
+* Default: `serif`
+* Description: font family
+
+### fontSize
+* Type: `number`
+* Default: `16`
+* Description: font size
+
+### fontWeight
+* Type: `string | number`
+* Default: `16`
+* Description: font weight, refer CSS `font-weight`
+
+### color
+* Type: `string`
+* Default: `#000000`
+* Description: font color
+
+### lineHeight
+* Type: `number`
+* Default: 1.5 * `fontSize`
+* Description: line height
+
+### textIndent
+* Type: `number`
+* Default: `0`
+* Description: text indentation for first line of fragment. interal new line of a fragment is not processed
+
+### textAlign
+* Type: `'center' | 'start' | 'end' | 'left' | 'right' | 'justify'`
+* Default: `start`
+* Description: text alignment, refer CSS `text-align`
+
+### textAlignLast
+* Type: `'center' | 'start' | 'end' | 'left' | 'right' | 'justify'`
+* Default: `start`
+* Description: when `textAlign` is `justify`, how the last line is aligned. internal new line of a fragment is also processed. refer CSS `text-align-last`
+
+
+### rtl
+* Type: `boolean`
+* Default: `false`
+* Description: if true, draw text from right to left
+
+### trim
+* Type: `boolean`
+* Default: `false`
+* Description: if true, trim white characters of text of a fragment
+
+### marginCollapse
+* Type: `boolean`
+* Default: `true`
+* Description: if true, the margin-top a fragment is the max of its own and the previous one. if `marginCollapse` of the previous fragment is `false`, margin of current fragment will not collapse
+
+### noHeadMargin
+* Type: `boolean`
+* Default: `false`
+* Description: ignore margin-top if a fragment is on the top of a frame
+
+
+## FragmentOptions
+### margin
+* Type: `number | { left?: number, right?: number, top?: number, bottom?: number }`
+* Default: `0`
+* Description: margin of a **fragment**
+
+### text
+* Type: `string`
+* Default: `''`
+* Description: text content of a fragment
diff --git a/docs/zh/example.html b/docs/zh/example.html
new file mode 100644
index 0000000..e69de29
diff --git a/docs/zh/options.md b/docs/zh/options.md
new file mode 100644
index 0000000..fdae75a
--- /dev/null
+++ b/docs/zh/options.md
@@ -0,0 +1,129 @@
+# 配置项
+
+## 导出函数原型
+```typescript
+export function computeTextFrames(options: FrameOptions): TextFrame[];
+export function renderFrame(context: CanvasRenderingContext2D, frame: TextFrame): void;
+```
+
+## FrameOptions
+### viewWidth
+* 类型: `number`
+* 默认值: `300`
+* 说明: 指定视图宽度,用于计算`canvasWidth`。除非显式提供`canvasWidth`参数,否则必须提供此参数。`viewWidth`应与要实际绘制文本的canvas的css宽度相等。
+
+### viewHeight
+* 类型: `number`
+* 默认值: `150`
+* 说明:指定视图高度。
+
+### canvasWidth
+* 类型: `number`
+* 默认值: 根据`viewWidth`计算。
+* 说明: canvas画布宽度,对应canvas.width,默认为`viewWidth * window.devicePixelRatio`。
+
+### canvasHeight
+* 类型: `number`
+* 默认值: 根据`viewHeight`计算。
+* 说明: canvas画布高度,对应canvas.height。
+
+### margin
+* 类型: `number | { left?: number, right?: number, top?: number, bottom?: number }`
+* 默认值: `0`
+* 说明: 每个分页(frame)的边距。注意,虽然`FragmentOptions`也有margin配置项,但二者并不是继承关系,而是作用于不同的对象:页面(frame)和文本段落(fragment)。
+
+### lineStartProhibitedMarks
+* 类型: `string`
+* 默认值: `、,,..。::;;!!??'"」』”’)]})】〗〕]}》〉–~~—·.‧•・//`
+* 说明: 禁止出现在行首的字符。
+
+### lineEndProhibitedMarks
+* 类型: `string`
+* 默认值: `「『“‘([{(【〖〔[〔《〈//`
+* 说明: 禁止出现在行尾的字符。
+
+### unbreakableRule
+* 类型: `RegExp`
+* 默认值: `/──|……|[\w\d]+/`
+* 说明: 符号分离禁则规则。如果提供,必须为正则表达式,正则表达式匹配则该认为该组合不能被拆分到新行,默认为破折号,省略号,和英文字母、数字组合。
+
+### fragments
+* 类型: `FragmentOptions[]`
+* 默认值: `null`
+* 说明: 文本段落数组。必须提供此参数。
+
+
+## FrameOptions & FragmentOptions
+以下配置项既可以在`FrameOptions`中指定,也可以在`FragmentOptions`中指定。当`FragmentOptions`未指定相关参数时,将继承`FrameOptions`的配置(再次强调,`margin`并不会继承)。
+
+### fontFamily
+* 类型: `string`
+* 默认值: `serif`
+* 说明: 字体名称。
+
+### fontSize
+* 类型: `number`
+* 默认值: `16`
+* 说明: 字体大小。
+
+### fontWeight
+* 类型: `string | number`
+* 默认值: `16`
+* 说明: 字体粗细。参考CSS `font-weight`。
+
+### color
+* 类型: `string`
+* 默认值: `#000000`
+* 说明: 字体颜色。
+
+### lineHeight
+* 类型: `number`
+* 默认值: 1.5倍`fontSize`
+* 说明: 行高。
+
+### textIndent
+* 类型: `number`
+* 默认值: `0`
+* 说明: 首行缩进。注意,对于每一个文本段落,仅首行缩进,即使文本段落中有换行符,新行也不会缩进。参考word中的软回车。若要分段落应提供多个文本段落。
+
+### textAlign
+* 类型: `'center' | 'start' | 'end' | 'left' | 'right' | 'justify'`
+* 默认值: `start`
+* 说明: 文本对齐方式。参考CSS `text-align`。
+
+### textAlignLast
+* 类型: `'center' | 'start' | 'end' | 'left' | 'right' | 'justify'`
+* 默认值: `start`
+* 说明: 当`textAlign`为`justify`时(两端对齐),段落最后一行的对齐方式。注意,这里的最后一行包括文本片段内部的换行符前的最后一行。参考CSS `text-align-last`。
+
+### rtl
+* 类型: `boolean`
+* 默认值: `false`
+* 说明: 是否从右到左渲染。
+
+### trim
+* 类型: `boolean`
+* 默认值: `false`
+* 说明: 是否自动删除文本片段首尾的空白符。注意,文本片段内部换行后行首尾空白符不会被清除。
+
+### marginCollapse
+* 类型: `boolean`
+* 默认值: `true`
+* 说明: 文本片段的上边距是否与前一文本片段的下边距折叠。注意,如果相邻的任一文本片段`marginCollapse`为false,则不会发生边距折叠。
+
+### noHeadMargin
+* 类型: `boolean`
+* 默认值: `false`
+* 说明: 当一个文本片段正好开始于一个空的页面时,是否忽略其上边距。
+
+
+## FragmentOptions
+### margin
+* 类型: `number | { left?: number, right?: number, top?: number, bottom?: number }`
+* 默认值: `0`
+* 说明: 文本片段的边距。注意,此配置项不继承于`FrameOptions`的`margin`选项。
+
+### text
+* 类型: `string`
+* 默认值: `''`
+* 说明: 该文本段落要绘制的文本内容。
diff --git a/esm.d.ts b/esm.d.ts
new file mode 100644
index 0000000..f7ccffa
--- /dev/null
+++ b/esm.d.ts
@@ -0,0 +1,8 @@
+import { FrameOptions, TextFrame } from "./src/options";
+
+export function computeTextFrames(options: FrameOptions): TextFrame[];
+export function renderFrame(
+ context: CanvasRenderingContext2D,
+ frame: TextFrame,
+ clear?: boolean
+): void;
diff --git a/esm.js b/esm.js
new file mode 100644
index 0000000..f6cfd89
--- /dev/null
+++ b/esm.js
@@ -0,0 +1,6 @@
+import { computeTextFrames, renderFrame } from './dist/esm/text-frame-min';
+
+export {
+ computeTextFrames,
+ renderFrame
+}
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..1c2425e
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..4f070ba
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,16 @@
+import { FrameOptions, FragmentOptions, TextFrame } from "./src/options";
+
+export as namespace TextFrame;
+
+export function computeTextFrames(options: FrameOptions): TextFrame[];
+export function renderFrame(
+ context: CanvasRenderingContext2D,
+ frame: TextFrame,
+ clear?: boolean
+): void;
+
+export {
+ FrameOptions,
+ FragmentOptions,
+ TextFrame
+}
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..65dc433
--- /dev/null
+++ b/index.js
@@ -0,0 +1,12 @@
+const devMode = process.env.NODE_ENV === 'development';
+
+const TextFrame = require('./dist/umd/text-frame-min')
+ , TextFrameDev = require('./dist/umd/text-frame');
+let _exports;
+if (devMode) {
+ _exports = TextFrameDev;
+}
+else {
+ _exports = TextFrame;
+}
+module.exports = _exports;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..7c755bd
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,484 @@
+{
+ "name": "text-frame",
+ "version": "0.0.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npm.taobao.org/@babel/code-frame/download/@babel/code-frame-7.10.4.tgz",
+ "integrity": "sha1-Fo2ho26Q2miujUnA8bSMfGJJITo=",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npm.taobao.org/@babel/helper-module-imports/download/@babel/helper-module-imports-7.10.4.tgz?cache=0&sync_timestamp=1593522826853&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhelper-module-imports%2Fdownload%2F%40babel%2Fhelper-module-imports-7.10.4.tgz",
+ "integrity": "sha1-TFxUvgS9MWcKc4J5fXW5+i5bViA=",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npm.taobao.org/@babel/helper-validator-identifier/download/@babel/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha1-p4x6clHgH2FlEtMbEK3PUq2l4NI=",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npm.taobao.org/@babel/highlight/download/@babel/highlight-7.10.4.tgz?cache=0&sync_timestamp=1593521095576&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fhighlight%2Fdownload%2F%40babel%2Fhighlight-7.10.4.tgz",
+ "integrity": "sha1-fRvf1ldTU4+r5sOFls23bZrGAUM=",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@babel/types": {
+ "version": "7.11.5",
+ "resolved": "https://registry.npm.taobao.org/@babel/types/download/@babel/types-7.11.5.tgz?cache=0&sync_timestamp=1598904189191&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Ftypes%2Fdownload%2F%40babel%2Ftypes-7.11.5.tgz",
+ "integrity": "sha1-2d5XfQElLXfGgAzuA57mT691Zi0=",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "lodash": "^4.17.19",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@rollup/plugin-babel": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npm.taobao.org/@rollup/plugin-babel/download/@rollup/plugin-babel-5.2.1.tgz",
+ "integrity": "sha1-IPyPiGTcDqocVXhAhFlgaAj3KSQ=",
+ "dev": true,
+ "requires": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@rollup/pluginutils": "^3.1.0"
+ }
+ },
+ "@rollup/pluginutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npm.taobao.org/@rollup/pluginutils/download/@rollup/pluginutils-3.1.0.tgz",
+ "integrity": "sha1-cGtFJO5tyLEDs8mVUz5a1oDAK5s=",
+ "dev": true,
+ "requires": {
+ "@types/estree": "0.0.39",
+ "estree-walker": "^1.0.1",
+ "picomatch": "^2.2.2"
+ }
+ },
+ "@types/estree": {
+ "version": "0.0.39",
+ "resolved": "https://registry.npm.taobao.org/@types/estree/download/@types/estree-0.0.39.tgz",
+ "integrity": "sha1-4Xfmme4bjCLSMXTKqnQiZEOJUJ8=",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "14.11.1",
+ "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.11.1.tgz?cache=0&sync_timestamp=1600368053055&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.11.1.tgz",
+ "integrity": "sha1-Vq+QKtFX52P5umPWccOc2jGTyDU=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-3.2.1.tgz",
+ "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz",
+ "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-2.4.2.tgz",
+ "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-1.9.3.tgz",
+ "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1598576050587&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz",
+ "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=",
+ "dev": true
+ },
+ "commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz",
+ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "estree-walker": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npm.taobao.org/estree-walker/download/estree-walker-1.0.1.tgz",
+ "integrity": "sha1-MbxdYSyWtwQQa0d+bdXYqhOMtwA=",
+ "dev": true
+ },
+ "find-cache-dir": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-3.3.1.tgz?cache=0&sync_timestamp=1583734806517&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffind-cache-dir%2Fdownload%2Ffind-cache-dir-3.3.1.tgz",
+ "integrity": "sha1-ibM/rUpGcNqpT4Vff74x1thP6IA=",
+ "dev": true,
+ "requires": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ }
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npm.taobao.org/find-up/download/find-up-4.1.0.tgz",
+ "integrity": "sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npm.taobao.org/fs-extra/download/fs-extra-8.1.0.tgz?cache=0&sync_timestamp=1591229972229&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffs-extra%2Fdownload%2Ffs-extra-8.1.0.tgz",
+ "integrity": "sha1-SdQ8RaiM2Wd2aMt74bRu/bjS4cA=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "fsevents": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npm.taobao.org/fsevents/download/fsevents-2.1.3.tgz",
+ "integrity": "sha1-+3OHA66NL5/pAMM4Nt3r7ouX8j4=",
+ "dev": true,
+ "optional": true
+ },
+ "graceful-fs": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.2.4.tgz",
+ "integrity": "sha1-Ila94U02MpWMRl68ltxGfKB6Kfs=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npm.taobao.org/js-tokens/download/js-tokens-4.0.0.tgz",
+ "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=",
+ "dev": true
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npm.taobao.org/jsonfile/download/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npm.taobao.org/locate-path/download/locate-path-5.0.0.tgz",
+ "integrity": "sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.20",
+ "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.20.tgz?cache=0&sync_timestamp=1597336017469&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.20.tgz",
+ "integrity": "sha1-tEqbYpe8tpjxxRo1RaKzs2jVnFI=",
+ "dev": true
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npm.taobao.org/make-dir/download/make-dir-3.1.0.tgz?cache=0&sync_timestamp=1587567875186&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmake-dir%2Fdownload%2Fmake-dir-3.1.0.tgz",
+ "integrity": "sha1-QV6WcEazp/HRhSd9hKpYIDcmoT8=",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.3.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-6.3.0.tgz",
+ "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=",
+ "dev": true
+ }
+ }
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npm.taobao.org/merge-stream/download/merge-stream-2.0.0.tgz",
+ "integrity": "sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=",
+ "dev": true
+ },
+ "mime": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npm.taobao.org/mime/download/mime-2.4.6.tgz?cache=0&sync_timestamp=1590596637243&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.4.6.tgz",
+ "integrity": "sha1-5bQHyQ20QvK+tbFiNz0Htpr/pNE=",
+ "dev": true
+ },
+ "opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npm.taobao.org/opener/download/opener-1.5.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fopener%2Fdownload%2Fopener-1.5.2.tgz",
+ "integrity": "sha1-XTfh81B3udysQwE3InGv3rKhNZg=",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npm.taobao.org/p-limit/download/p-limit-2.3.0.tgz?cache=0&sync_timestamp=1594559666231&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fp-limit%2Fdownload%2Fp-limit-2.3.0.tgz",
+ "integrity": "sha1-PdM8ZHohT9//2DWTPrCG2g3CHbE=",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npm.taobao.org/p-locate/download/p-locate-4.1.0.tgz",
+ "integrity": "sha1-o0KLtwiLOmApL2aRkni3wpetTwc=",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npm.taobao.org/p-try/download/p-try-2.2.0.tgz",
+ "integrity": "sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npm.taobao.org/path-exists/download/path-exists-4.0.0.tgz",
+ "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npm.taobao.org/path-parse/download/path-parse-1.0.6.tgz",
+ "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npm.taobao.org/picomatch/download/picomatch-2.2.2.tgz",
+ "integrity": "sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=",
+ "dev": true
+ },
+ "pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npm.taobao.org/pkg-dir/download/pkg-dir-4.2.0.tgz",
+ "integrity": "sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM=",
+ "dev": true,
+ "requires": {
+ "find-up": "^4.0.0"
+ }
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npm.taobao.org/randombytes/download/randombytes-2.1.0.tgz",
+ "integrity": "sha1-32+ENy8CcNxlzfYpE0mrekc9Tyo=",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "resolve": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.17.0.tgz",
+ "integrity": "sha1-sllBtUloIxzC0bt2p5y38sC/hEQ=",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "rollup": {
+ "version": "2.28.1",
+ "resolved": "https://registry.npm.taobao.org/rollup/download/rollup-2.28.1.tgz?cache=0&sync_timestamp=1600668802635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Frollup%2Fdownload%2Frollup-2.28.1.tgz",
+ "integrity": "sha1-zu3KPNsBPC+o8i+ViinCAzaBWeo=",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.1.2"
+ }
+ },
+ "rollup-plugin-serve": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npm.taobao.org/rollup-plugin-serve/download/rollup-plugin-serve-1.0.4.tgz",
+ "integrity": "sha1-F2ZZdBhfkAfsrrCDXE6JYS4E0jQ=",
+ "dev": true,
+ "requires": {
+ "mime": ">=2.4.6",
+ "opener": "1"
+ }
+ },
+ "rollup-plugin-terser": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npm.taobao.org/rollup-plugin-terser/download/rollup-plugin-terser-7.0.2.tgz?cache=0&sync_timestamp=1599268471748&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Frollup-plugin-terser%2Fdownload%2Frollup-plugin-terser-7.0.2.tgz",
+ "integrity": "sha1-6Pu6SGmYGy3DWufopQLVxsBNMk0=",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "jest-worker": "^26.2.1",
+ "serialize-javascript": "^4.0.0",
+ "terser": "^5.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
+ "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
+ "dev": true
+ },
+ "jest-worker": {
+ "version": "26.3.0",
+ "resolved": "https://registry.npm.taobao.org/jest-worker/download/jest-worker-26.3.0.tgz?cache=0&sync_timestamp=1597057408381&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjest-worker%2Fdownload%2Fjest-worker-26.3.0.tgz",
+ "integrity": "sha1-fIqX5PQ2S08F7YvKjKDCTeCRhx8=",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^7.0.0"
+ }
+ },
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npm.taobao.org/serialize-javascript/download/serialize-javascript-4.0.0.tgz?cache=0&sync_timestamp=1599740699862&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fserialize-javascript%2Fdownload%2Fserialize-javascript-4.0.0.tgz",
+ "integrity": "sha1-tSXhI4SJpez8Qq+sw/6Z5mb0sao=",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1598611708628&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
+ "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "rollup-plugin-typescript2": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npm.taobao.org/rollup-plugin-typescript2/download/rollup-plugin-typescript2-0.27.3.tgz",
+ "integrity": "sha1-zZRVrAJtMlsgxXKNLMVKCKdxtos=",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^3.1.0",
+ "find-cache-dir": "^3.3.1",
+ "fs-extra": "8.1.0",
+ "resolve": "1.17.0",
+ "tslib": "2.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.2.1.tgz?cache=0&sync_timestamp=1589129010497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsafe-buffer%2Fdownload%2Fsafe-buffer-5.2.1.tgz",
+ "integrity": "sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY=",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz",
+ "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npm.taobao.org/source-map-support/download/source-map-support-0.5.19.tgz",
+ "integrity": "sha1-qYti+G3K9PZzmWSMCFKRq56P7WE=",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-5.5.0.tgz?cache=0&sync_timestamp=1598611708628&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-5.5.0.tgz",
+ "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "terser": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npm.taobao.org/terser/download/terser-5.3.2.tgz?cache=0&sync_timestamp=1600354856380&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser%2Fdownload%2Fterser-5.3.2.tgz",
+ "integrity": "sha1-9L6pDrkpRbKgKM7veRgbm7WG568=",
+ "dev": true,
+ "requires": {
+ "commander": "^2.20.0",
+ "source-map": "~0.6.1",
+ "source-map-support": "~0.5.12"
+ }
+ },
+ "to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-2.0.0.tgz?cache=0&sync_timestamp=1580550347606&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fto-fast-properties%2Fdownload%2Fto-fast-properties-2.0.0.tgz",
+ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
+ "dev": true
+ },
+ "tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npm.taobao.org/tslib/download/tslib-2.0.1.tgz?cache=0&sync_timestamp=1596754132993&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-2.0.1.tgz",
+ "integrity": "sha1-QQ6w0RPltjVkkO7HSWA3JbAhtD4=",
+ "dev": true
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npm.taobao.org/typescript/download/typescript-4.0.3.tgz?cache=0&sync_timestamp=1601029293027&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftypescript%2Fdownload%2Ftypescript-4.0.3.tgz",
+ "integrity": "sha1-FTu9Ro7wdyXB35x36LRT+NNqu6U=",
+ "dev": true
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npm.taobao.org/universalify/download/universalify-0.1.2.tgz",
+ "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4b4f4d7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@mattuy/text-frame",
+ "version": "0.0.1",
+ "private": false,
+ "description": "a javascript tool to split long text into pages, with typesetting prohibition processed and unicode full support.\n一个将长文本分成若干页的js工具,处理排版禁则,支持Unicode",
+ "main": "index.js",
+ "directories": {
+ "example": "example"
+ },
+ "scripts": {
+ "build": "rollup -c rollup.config.js --environment NODE_ENV:development",
+ "dist": "rollup -c rollup.config.js --environment NODE_ENV:production",
+ "debug": "rollup -c rollup.config.js --environment NODE_ENV:development --watch",
+ "install": "npm run build && npm run dist",
+ "example": "rollup -c rollup.config.js --environment NODE_ENV:development --environment SERVE"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/mattuylee/text-frame.git"
+ },
+ "keywords": [
+ "text pagination",
+ "typesetting",
+ "canvas",
+ "Chinese",
+ "文本排版",
+ "排版禁则",
+ "分页",
+ "中文"
+ ],
+ "author": "mattuy ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/mattuylee/text-frame/issues"
+ },
+ "homepage": "https://github.com/mattuylee/text-frame#readme",
+ "devDependencies": {
+ "@rollup/plugin-babel": "^5.2.1",
+ "rollup": "^2.28.1",
+ "rollup-plugin-serve": "^1.0.4",
+ "rollup-plugin-terser": "^7.0.2",
+ "rollup-plugin-typescript2": "^0.27.3",
+ "typescript": "^4.0.3"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..5aff873
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,42 @@
+import * as path from 'path';
+import { terser } from "rollup-plugin-terser";
+import typescript from 'rollup-plugin-typescript2';
+import serve from 'rollup-plugin-serve';
+
+const devMode = process.env.NODE_ENV === 'development';
+const shouldServe = !!process.env.SERVE;
+const filename = 'text-frame' + (devMode ? '' : '-min') + '.js';
+const plugins = [
+ typescript({
+ clean: true,
+ tsconfig: './tsconfig.json'
+ }),
+ !devMode && terser(),
+ shouldServe && serve({
+ open: true,
+ contentBase: './',
+ openPage: '/example/index.html',
+ port: 5000
+ })
+].filter(Boolean);
+
+export default {
+ input: 'src/text-frame.ts',
+ output: [
+ {
+ file: path.resolve('dist', 'esm', filename),
+ format: 'esm',
+ sourcemap: devMode
+ },
+ {
+ file: path.resolve('dist', 'umd', filename),
+ format: 'umd',
+ name: 'TextFrame',
+ sourcemap: devMode
+ }
+ ],
+ plugins: plugins,
+ watch: {
+ include: ['src/**/*', 'example/*']
+ }
+}
\ No newline at end of file
diff --git a/src/options.ts b/src/options.ts
new file mode 100644
index 0000000..a496825
--- /dev/null
+++ b/src/options.ts
@@ -0,0 +1,145 @@
+export enum TextAlign {
+ center = 'center',
+ start = 'start',
+ end = 'end',
+ left = 'left',
+ right = 'right',
+ justify = 'justify'
+}
+export interface BaseOptions {
+ color?: string
+ rtl?: boolean
+ trim?: boolean
+ fontFamily?: string
+ fontWeight?: string | number
+ fontSize?: number
+ lineHeight?: number
+ textIndent?: number
+ textAlign?: TextAlign | 'center' | 'start' | 'end' | 'left' | 'right' | 'justify'
+ textAlignLast?: TextAlign | 'center' | 'start' | 'end' | 'left' | 'right' | 'justify'
+ margin?: number | Margin
+ marginCollapse?: boolean
+ noHeadMargin?: boolean
+}
+
+export interface FragmentOptions extends BaseOptions {
+ text: string
+}
+
+export interface Margin {
+ left?: number
+ right?: number
+ top?: number
+ bottom?: number
+}
+
+export interface FrameOptions extends BaseOptions {
+ viewWidth?: number
+ viewHeight?: number
+ canvasHeight?: number
+ canvasWidth?: number
+ lineStartProhibitedMarks?: string
+ lineEndProhibitedMarks?: string
+ unbreakableRule?: RegExp
+ fragments: FragmentOptions[]
+}
+
+export interface FrameLine {
+ offset: { x: number, y: number }
+ fragmentOptions: FragmentOptions
+ isLastLine: boolean
+ chars: string[]
+}
+
+export interface TextFrame {
+ options: FrameOptions
+ lines: FrameLine[]
+}
+
+const
+ defaultOptionBase: BaseOptions = {
+ color: '#000000',
+ fontFamily: 'serif',
+ fontWeight: 'normal',
+ fontSize: 16,
+ rtl: false,
+ margin: 0,
+ marginCollapse: true,
+ noHeadMargin: false,
+ textIndent: 0,
+ textAlign: 'justify',
+ textAlignLast: 'start'
+ }
+ , defaultOptions: FrameOptions = {
+ ...defaultOptionBase,
+ viewWidth: 300,
+ viewHeight: 150,
+ lineStartProhibitedMarks: `、,,..。::;;!!??'"」』”’)]})】〗〕]}》〉–~~—·.‧•・//`,
+ lineEndProhibitedMarks: `「『“‘([{(【〖〔[〔《〈//`,
+ unbreakableRule: /──|……|[\w\d]+/,
+ fragments: null
+ };
+
+function resolveBaseOptions(options: BaseOptions) {
+ // we don't cache devicePixelRatio globally, since it may change
+ const dpi = devicePixelRatio || 1;
+ const resolvedOptions: BaseOptions = {
+ ...defaultOptionBase,
+ ...(options || Object())
+ };
+ resolvedOptions.textIndent = (resolvedOptions.textIndent | 0) * dpi;
+ if (resolvedOptions.fontSize > 0) {
+ resolvedOptions.fontSize *= dpi;
+ }
+ else {
+ resolvedOptions.fontSize = defaultOptions.fontSize * dpi;
+ }
+ if (resolvedOptions.lineHeight > 0) {
+ resolvedOptions.lineHeight *= dpi;
+ }
+ else {
+ resolvedOptions.lineHeight = resolvedOptions.fontSize * 1.5;
+ }
+ if (!(resolvedOptions.textAlign in TextAlign)) {
+ resolvedOptions.textAlign = TextAlign.start;
+ }
+ if (typeof resolvedOptions.margin === 'object' && resolvedOptions !== null) {
+ for (const s of ['left', 'right', 'top', 'bottom']) {
+ resolvedOptions.margin[s] = (resolvedOptions.margin[s] | 0) * dpi;
+ }
+ }
+ else {
+ const margin = (resolvedOptions.margin as number | 0) * dpi;
+ resolvedOptions.margin = {
+ left: margin,
+ right: margin,
+ top: margin,
+ bottom: margin
+ };
+ }
+ return resolvedOptions;
+}
+export function resolveOptions(options: FrameOptions) {
+ const dpi = devicePixelRatio
+ , resolvedOptions: FrameOptions = {
+ ...defaultOptions,
+ ...resolveBaseOptions(options)
+ };
+ resolvedOptions.canvasWidth = options.canvasWidth || resolvedOptions.viewWidth * dpi;
+ resolvedOptions.canvasHeight = options.canvasHeight || resolvedOptions.viewHeight * dpi;
+ return resolvedOptions;
+}
+export function resolveFragmentOptions(slideOps: FrameOptions, fragOps: FragmentOptions) {
+ // margin is not inherited from frame options
+ const { margin: _, ...fallback } = slideOps;
+ // for ignored config, fallback to global option, then fallback to default option
+ let resolvedOptions: FragmentOptions = { ...fallback, ...fragOps };
+ resolvedOptions.text = resolvedOptions.text || '';
+ resolvedOptions.text =
+ resolvedOptions.text.replace('\r\n', '\n').replace('\r', '\n');
+ resolvedOptions = resolveBaseOptions(resolvedOptions) as FragmentOptions;
+ if (resolvedOptions.trim) {
+ resolvedOptions.text = resolvedOptions.text.trim();
+ }
+ return resolvedOptions;
+}
\ No newline at end of file
diff --git a/src/text-frame.ts b/src/text-frame.ts
new file mode 100644
index 0000000..11d910f
--- /dev/null
+++ b/src/text-frame.ts
@@ -0,0 +1,333 @@
+import { TextFrame, FrameLine, FragmentOptions, Margin, FrameOptions, TextAlign, resolveOptions, resolveFragmentOptions } from "./options";
+const ctx = document.createElement('canvas').getContext('2d');
+
+export function computeTextFrames(options: FrameOptions) {
+ const ops = resolveOptions(options || {} as any)
+ , frames: TextFrame[] = []
+ , frameMargin = ops.margin as Margin
+ , maxFrameX = ops.canvasWidth - 1 - frameMargin.right
+ , maxFrameY = ops.canvasHeight - 1 - frameMargin.bottom;
+ let cursor = { x: 0, y: 0 }
+ , textIndex = 0
+ , maxLineX = 0
+ , remainingMargin = 0
+ , previousMarginCollapse = false
+ , currFragment: FragmentOptions
+ , currentText: string
+ , currentLine: FrameLine
+ , currentFrame: TextFrame;
+
+ /**
+ * get next char.
+ * JavaScript encode string with UTF-16, where one unicode code point may be
+ * encoded with two chars(such as emoji), so we need to deal with that case.
+ * Refrence(English): @see https://en.wikipedia.org/wiki/UTF-16
+ * Refrence(Chinese): @see https://zh.wikipedia.org/wiki/UTF-16
+ */
+ function nextChar() {
+ if (!currentText) { return ''; }
+ const codePoint = currentText.codePointAt(textIndex);
+ if (codePoint === undefined) { return ''; }
+ const charLen = String.fromCodePoint(codePoint).length;
+ return currentText.slice(textIndex, textIndex + charLen);
+ }
+
+ // move cursor to a new frame
+ function newFrame() {
+ if (currentFrame) {
+ finishFrame();
+ }
+ currentFrame = {
+ options: ops,
+ lines: []
+ }
+ currentLine = null;
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ cursor = { x: frameMargin.left, y: frameMargin.top };
+ };
+
+ // move cursor to head of a new line
+ function newLine() {
+ const fragOps = currFragment;
+ if (!currentFrame) { newFrame(); }
+ finishLine();
+ if (currentFrame.lines.length === 0) {
+ // if there is no line in current frame, just start a new line without
+ // checking available height, since a frame must be high enouth for one
+ // line
+ cursor = {
+ x: frameMargin.left + fragOps.margin['left'],
+ y: frameMargin.top
+ }
+ }
+ else {
+ // cursor.y are based on start of text box, but maxFrameY is max y which
+ // the end of text box can reach, so we need to minus an extra lineHeight
+ const availableHeight = maxFrameY - (cursor.y + fragOps.lineHeight);
+ if (availableHeight < fragOps.lineHeight) {
+ newFrame();
+ // we don't plus the top margin of current fragment, because fragment
+ // verticle margin should only be computed at the starting of fragment.
+ cursor = {
+ x: frameMargin.left + fragOps.margin['left'],
+ y: frameMargin.top
+ };
+ }
+ else {
+ cursor.x = frameMargin.left + fragOps.margin['left'];
+ cursor.y += fragOps.lineHeight;
+ }
+ }
+ currentLine = {
+ chars: [],
+ isLastLine: false,
+ fragmentOptions: fragOps,
+ offset: { ...cursor },
+ }
+ };
+
+ function finishLine() {
+ if (currentLine) {
+ // if a line exist currently, commit it
+ currentFrame.lines.push(currentLine);
+ currentLine = null;
+ }
+ }
+ function finishLastLine() {
+ if (currentLine) {
+ currentLine.isLastLine = true;
+ }
+ finishLine();
+ }
+
+ // flush work on current frame
+ function finishFrame() {
+ if (currentFrame) {
+ if (currentLine) { finishLine(); }
+ frames.push(currentFrame);
+ currentFrame = null;
+ }
+ }
+
+
+ for (let fragOps of (ops.fragments || [])) {
+ // flush work on previous fragment
+ finishLastLine();
+ fragOps = currFragment = resolveFragmentOptions(options, fragOps);
+ ctx.font = fragOps.fontWeight + ' '
+ + fragOps.fontSize + 'px '
+ + fragOps.fontFamily;
+ newLine();
+ if (currentFrame.lines.length === 0) {
+ // don't let new frame affected by remaining margin from previous frame
+ remainingMargin = 0;
+ }
+ if (fragOps.marginCollapse && previousMarginCollapse) {
+ remainingMargin = Math.max(fragOps.margin['top'], remainingMargin);
+ }
+ else {
+ remainingMargin += fragOps.margin['top'];
+ }
+ if (currentFrame.lines.length > 0) {
+ if (cursor.y + remainingMargin + fragOps.lineHeight > maxFrameY + 1) {
+ // there is no enough space on current frame, get a new one
+ currentLine = null;
+ finishFrame();
+ newLine();
+ }
+ else {
+ cursor.y += remainingMargin;
+ }
+ }
+ // not "else if". we check, or maybe re-check whether current frame is
+ // empty, because current frame may be resetted above
+ if (currentFrame.lines.length === 0 && !fragOps.noHeadMargin) {
+ // current frame is empty, we don't create new frame anyway, so we can
+ // skip checking space
+ cursor.y = frameMargin.top + fragOps.margin['top'];
+ }
+ else {
+ // if current frame is not empty, we have processed above; if
+ // fragOps.noHeadMargin is true, the cursor is set correctly when we
+ // create current frame. so there is nothing to do.
+ }
+ if (fragOps.textIndent > 0) {
+ cursor.x += fragOps.textIndent;
+ }
+ // we moved cursor, and the slot of line start may have changed, reset it
+ currentLine.offset.x = cursor.x;
+ currentLine.offset.y = cursor.y;
+ maxLineX = maxFrameX - fragOps.margin['right'];
+ currentText = fragOps.text;
+ textIndex = 0;
+
+ while (textIndex < currentText.length) {
+ let currentChar = nextChar()
+ , currentCharLength = currentChar.length
+ , currentCharWidth = ctx.measureText(currentChar).width
+ , availableWidth = maxLineX + 1 - cursor.x;
+ if (currentChar === '\n') {
+ // current char is a carrige return, just move cusor to next line.
+ // even if the last char of current line if a prohibition of line end,
+ // we don't process it, since the paragraph has ended.
+ finishLastLine();
+ newLine();
+ currentChar = null;
+ currentCharWidth = 0;
+ // we don't indent text since it's not the begining of a paragraph.
+ }
+ else if (availableWidth < currentCharWidth) {
+ // there is no enough space to place current char, we need to turn to a
+ // new line.
+ // but before that, we have to process the prohibition of line start
+ // and line end.
+ let lineStartChar = currentChar, lineEndChar, newIndex = textIndex;
+ for (let i = currentLine.chars.length - 1; i >= 0; --i) {
+ lineEndChar = currentLine.chars[i];
+ // we check whether there is a prohibition, if true, try to fix.
+ // if a prohibition found, we try to fix it through moving the
+ // line-end char of current line to the start of next line. then
+ // check, repeat.
+ // if we can't fix when current line has only one char left, just do
+ // nothing as if there was no prohibition found.
+ if (!ops.lineStartProhibitedMarks.includes(lineStartChar)
+ && !ops.lineEndProhibitedMarks.includes(lineEndChar)
+ && !ops.unbreakableRule.test(lineEndChar + lineStartChar)) {
+ // fix
+ currentChar = lineStartChar;
+ currentCharLength = currentChar.length;
+ currentCharWidth = ctx.measureText(currentChar).width;
+ textIndex = newIndex;
+ currentLine.chars = currentLine.chars.slice(0, i + 1);
+ break;
+ }
+ newIndex -= lineStartChar.length;
+ lineStartChar = lineEndChar;
+ }
+ newLine();
+ }
+
+ if (currentChar) {
+ currentLine.chars.push(currentChar);
+ }
+ cursor.x += currentCharWidth;
+ textIndex += currentCharLength;
+ }
+ remainingMargin = fragOps.margin['bottom'];
+ previousMarginCollapse = fragOps.marginCollapse;
+ }
+ finishLastLine();
+ finishFrame();
+ return frames;
+}
+
+
+// prepare canvas to render text.
+function prepareCanvas(
+ context: CanvasRenderingContext2D,
+ fragOptions: FragmentOptions,
+ line: FrameLine) {
+ const mergedOptions = fragOptions;
+ if (mergedOptions) {
+ context.font = mergedOptions.fontWeight + ' '
+ + mergedOptions.fontSize + 'px '
+ + mergedOptions.fontFamily;
+ context.fillStyle = mergedOptions.color;
+ // since context.direction is an experimental technology, we cannot rely on it
+ context.direction = 'ltr';
+ let textAlign = mergedOptions.textAlign;
+ if (line.isLastLine && mergedOptions.textAlign === 'justify') {
+ textAlign = mergedOptions.textAlignLast;
+ }
+ if (textAlign === 'start') {
+ context.textAlign = mergedOptions.rtl ? 'right' : 'left';
+ }
+ else if (textAlign === 'end') {
+ context.textAlign = mergedOptions.rtl ? 'left' : 'right';
+ }
+ else if (['center', 'left', 'right'].includes(textAlign)) {
+ context.textAlign = textAlign as any;
+ }
+ else {
+ context.textAlign = 'left';
+ }
+ context.textBaseline = 'middle';
+ }
+}
+
+// finish work on current line
+function renderLine(
+ context: CanvasRenderingContext2D,
+ options: FrameOptions,
+ line: FrameLine) {
+ if (line) {
+ const fragOps = line.fragmentOptions
+ , chars = fragOps.rtl
+ ? line.chars.slice(0).reverse()
+ : line.chars
+ , frameMargin = options.margin as Margin
+ , fragMargin = fragOps.margin as Margin;
+ // draw text
+ prepareCanvas(context, fragOps, line);
+ let { x: offsetX, y: offsetY } = line.offset
+ , isJustifyMode = fragOps.textAlign === TextAlign.justify
+ , textIndent = offsetX - frameMargin.left - fragOps.margin['left'];
+ // offsetY is based on top of text box, and context.textBaseline is
+ // 'middle', so plus a half line height
+ offsetY += fragOps.lineHeight / 2;
+ if (isJustifyMode && line.isLastLine && fragOps.textAlignLast !== TextAlign.justify) {
+ isJustifyMode = false;
+ }
+ if (isJustifyMode) {
+ const contentWidth = context.measureText(chars.join('')).width
+ , remainWidth = (options.canvasWidth - contentWidth
+ - frameMargin.left - frameMargin.right - fragMargin.left
+ - fragMargin.right - textIndent)
+ , gap = remainWidth / (chars.length - 1);
+ if (fragOps.rtl) {
+ // initially, we assume using left-to-right mode, but it's right-to-left
+ // mode now, we'll reverse the text and recompute offsetX, and we have
+ // to "move" text indention to right
+ offsetX -= textIndent;
+ }
+ for (let i = 0; i < chars.length; ++i) {
+ context.fillText(chars[i], Math.round(offsetX), offsetY);
+ offsetX += context.measureText(chars[i]).width + gap;
+ }
+ }
+ else {
+ // we use context.textAlign not fragOps.textAlign, because the first one
+ // has been normalized when preparing canvas
+ if (context.textAlign === 'center') {
+ offsetX = context.canvas.width / 2;
+ }
+ else if (context.textAlign === 'right') {
+ offsetX = (options.canvasWidth - frameMargin.right
+ - fragOps.margin['right'] - textIndent);
+ }
+ context.fillText(chars.join(''), offsetX, offsetY);
+ }
+ line = null;
+ }
+}
+
+export function renderFrame(context: CanvasRenderingContext2D, frame: TextFrame, clear?: boolean) {
+ if (!context) {
+ throw Error("error: text-frame - must provide drawing context!");
+ }
+ if (!frame) {
+ context.clearRect(0, 0, context.canvas.width, context.canvas.height);
+ return;
+ }
+ const canvasWidth = Math.round(frame.options.canvasWidth)
+ , canvasHeight = Math.round(frame.options.canvasHeight);
+ if (clear
+ || context.canvas.width !== canvasWidth
+ || context.canvas.height !== canvasHeight) {
+ context.canvas.width = canvasWidth
+ context.canvas.height = canvasHeight
+ }
+ for (const line of frame.lines) {
+ renderLine(context, frame.options, line);
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..aee0d40
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,72 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+ "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+ "lib": ["ES5", "ES2015", "DOM"], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ // "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ // "outDir": "./", /* Redirect output structure to the directory. */
+ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": false, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+
+ /* Source Map Options */
+ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+
+ /* Advanced Options */
+ // "skipLibCheck": true, /* Skip type checking of declaration files. */
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ },
+ "exclude": [
+ "node_modules/**/*"
+ ]
+}