]> git.kianting.info Git - clo/blob - src/libclo/index.ts
add line-breaking algorithm initially
[clo] / src / libclo / index.ts
1 import { isBoxedPrimitive, isKeyObject, isStringObject } from "util/types";
2 import {tkTree} from "../parser";
3 import {FontStyle, TextStyle, TextWeight, fontStyleTofont} from "../canva";
4 import { JSDOM } from "jsdom";
5 import * as fontkit from "fontkit";
6 import * as util from "node:util";
7 import * as breakLines from "./breakLines";
8
9 /**
10 * TYPES
11 */
12
13 /**
14 * text direction
15 * LTR - left to right
16 * TTB - top to bottom
17 * etc.
18 */
19 export enum Direction{
20 LTR,
21 RTL,
22 TTB,
23 BTT,
24 }
25
26 /**
27 * Horizonal glue.
28 * - stretchFactor : the stretch factor in float
29 */
30 export interface HGlue{
31 stretchFactor: number
32 }
33
34 export interface BreakPoint{
35 original : BoxesItem,
36 newLined : BoxesItem
37 }
38
39 export type BoxesItem = HGlue | Box | BreakPoint | BoxesItem[] ;
40
41 /**
42 * frame box is a subclass of box
43 * - directionInsideLine : text direction inside a line
44 * - baselineskip : the distance between baselines in px
45 */
46 export interface FrameBox extends Box{
47 directionInsideLine : Direction,
48 baseLineskip : number | null,
49 }
50
51 export interface CharBox extends Box{
52 minX: number,
53 maxX: number,
54 minY: number,
55 maxY: number,
56
57 }
58
59 /**
60 * a basic Box
61 * - x :
62 * - y :
63 * - textStyle :
64 * - direction :
65 * - width : x_advance
66 * - content :
67 */
68 export interface Box{
69 x : number | null,
70 y : number | null,
71 textStyle : TextStyle | null,
72 direction : Direction,
73 width : number,
74 height : number,
75 content : string | Box[] | null,
76 }
77
78
79 /**
80 * DEFAULT CONST PART
81 */
82 export const A4_IN_PX = {"width" : 793.7,
83 "height" : 1122.5};
84
85 export const defaultTextStyle : TextStyle = {
86 family : "FreeSerif",
87 size : ptToPx(12),
88 textWeight : TextWeight.REGULAR,
89 fontStyle : FontStyle.ITALIC,
90 }
91
92 export const defaultFrameStyle : FrameBox = {
93 directionInsideLine : Direction.LTR,
94 direction : Direction.TTB,
95 baseLineskip : ptToPx(15),
96 textStyle : defaultTextStyle,
97 x : A4_IN_PX.width * 0.10,
98 y : A4_IN_PX.height * 0.10,
99 width : A4_IN_PX.width * 0.80,
100 height : A4_IN_PX.height * 0.80,
101 content : null,
102 };
103
104 /**
105 * definition for cjk scripts
106 * - Hani : Han Character
107 * - Hang : Hangul
108 * - Bopo : Bopomofo
109 * - Kana : Katakana
110 * - Hira : Hiragana
111 */
112 export const cjkvBlocksInRegex = ["Hani", "Hang", "Bopo", "Kana", "Hira"];
113
114 export const cjkvRegexPattern = new RegExp("((?:" +
115 cjkvBlocksInRegex.map((x)=>"\\p{Script_Extensions="+x+"}").join("|") + ")+)", "gu");
116 /**
117 * FUNCTION PART
118 */
119 /**
120 * convert from ptToPx
121 * @param pt pt size value
122 * @returns the corresponding px value
123 */
124 export function ptToPx(pt : number) : number{
125 return pt * 4.0 / 3.0;
126 }
127
128
129
130 /**
131 * REGISTER PART
132 */
133
134 /**
135 * convert '\n\n' to newline command ["nl"]
136 * @param arr the input `tkTree`
137 * @param clo the `Clo` object
138 * @returns the input tktree
139 */
140 export function twoReturnsToNewline(arr : tkTree, clo : Clo): tkTree{
141 var middle : tkTree = [];
142
143 for (let i = 0; i < arr.length; i++) {
144 var item = arr[i];
145 if (!Array.isArray(item)){
146 middle = middle.concat(item.split(/(\n\n)/g));
147 }
148 else{
149 middle.push(item);
150 }
151 }
152
153 var result : tkTree = [];
154 for (let j = 0; j < middle.length; j++){
155 var item = middle[j];
156 if (!Array.isArray(item) && item == "\n\n"){
157 result.push(["nl"]); // push a newline command to the result `tkTree`
158 }
159 else{
160 result.push(middle[j]);
161 }
162 }
163
164 return result;
165 }
166
167 /**
168 * split CJKV and non-CJKV
169 *
170 * @param arr : input tkTree
171 * @returns a splitted tkTree (by CJK and NonCJK)
172 * - Examples:
173 * ```
174 * [`many臺中daylight`] => [`many`, `臺中`, `dahylight`]
175 * ```
176 */
177 export function splitCJKV(arr : tkTree, clo : Clo): tkTree{
178 var result : tkTree = [];
179 for (let i = 0; i < arr.length; i++) {
180 var item = arr[i];
181 if (!Array.isArray(item)){
182 result = result.concat(item.split(cjkvRegexPattern));
183 }
184 else{
185 result.push(item);
186 }
187 }
188
189 return result;
190 }
191
192 /**
193 * hyphenation for a clo document
194 * @param arr the array for a `tkTree`
195 * @param clo the Clo object
196 */
197 export function hyphenForClo(arr : tkTree, clo : Clo): tkTree{
198 let hyphenLanguage : string = clo.attrs["hyphenLanguage"];
199 let res = hyphenTkTree(arr, hyphenLanguage);
200 return res;
201
202 }
203
204 /**
205 * convert spaces to Breakpoint
206 * \s+ => ["bp" [\s+] ""]
207 * @param arr the tkTree input text stream
208 * @param clo the Clo object
209 * @returns the converted object
210 */
211 export function spacesToBreakpoint(arr : tkTree, clo : Clo) : tkTree{
212 let spacePattern = /^([ \t]+)$/g;
213 var result : tkTree = [];
214 for (let i = 0; i < arr.length; i++){
215 var item = arr[i];
216 if (!Array.isArray(item) && item.match(spacePattern)){
217 // push a breakpoint command to the result `tkTree`
218 result.push([ 'bp', [["hglue", "0.1"], item] , "" ]);
219 }
220 else{
221 result.push(item);
222 }
223 }
224
225 return result;
226 }
227
228 /**
229 * remove all the `` (empty string) in the arr
230 * @param arr the tkTree to be filtered
231 * @param clo the Clo file
232 */
233 export function filterEmptyString(arr : tkTree, clo : Clo) : tkTree{
234 if (Array.isArray(arr)){
235 arr.filter((x)=>{return x != ``;});
236 }
237
238 return arr;
239 }
240
241
242 /**
243 * OTHER FUNCTIONS
244 */
245
246 /**
247 * hyphenate for a tkTree
248 * - hyphenation => ["bp", "", "-"]
249 * @param arr the tkTree array
250 * @param lang ISO 639 code for the language
251 */
252 export function hyphenTkTree(arr : tkTree, lang: string) : tkTree{
253 // import corresponding hyphen language data and function
254 let hyphen = require("hyphen/"+lang);
255
256 let result :tkTree[] = [];
257 for (let i = 0; i < arr.length; i++) {
258 let element = arr[i];
259 let splitter = "分"; // a CJKV
260 if (!Array.isArray(element)){
261 let hyphenatedElement : string = hyphen.hyphenateSync(element, {hyphenChar :splitter});
262 let hyphenatedSplitted : tkTree = hyphenatedElement.split(splitter);
263 var newSplitted : tkTree = [];
264 for (var j=0; j<hyphenatedSplitted.length-1;j++){
265 newSplitted.push(hyphenatedSplitted[j]);
266 // "bp" for breakpoint
267 newSplitted.push(["bp", "", "-"]); //insert a breakable point (bp) mark
268 }
269 newSplitted.push(hyphenatedSplitted[hyphenatedSplitted.length-1]);
270
271 result = result.concat(newSplitted);
272
273 }else{
274 result.push(element);
275 }
276
277 }
278
279 return result;
280 }
281
282 /**
283 * calculate the text width and Height with a given `TextStyle`
284 * @param preprocessed
285 * @param defaultFontStyle
286 */
287 export async function calculateTextWidthHeight(element : tkTree, style : TextStyle): Promise<BoxesItem[]> {
288 var res = [];
289
290 for (var i=0; i<element.length; i++){
291 res.push(await calculateTextWidthHeightAux(element[i], style));
292 }
293
294 res = res.flat();
295
296 return res;
297 }
298
299
300 /**
301 * calculate the text width and Height with a given `TextStyle`
302 * @param preprocessed
303 * @param defaultFontStyle
304 */
305 export async function calculateTextWidthHeightAux(element : tkTree, style : TextStyle): Promise<BoxesItem> {
306 var result : BoxesItem = [];
307
308
309
310 let fontPair = fontStyleTofont(style);
311 if (fontPair.path.match(/\.ttc$/)){
312 var font = await fontkit.openSync(fontPair.path, fontPair.psName);
313 }
314 else{
315 var font = await fontkit.openSync(fontPair.path);
316 }
317 if (!Array.isArray(element)){
318 var run = font.layout(element, undefined, undefined, undefined, "ltr");
319
320
321
322 for (var j=0;j<run.glyphs.length;j++){
323 let runGlyphsItem = run.glyphs[j];
324
325
326 let item : CharBox = {
327 x : null,
328 y : null,
329 textStyle : style,
330 direction : Direction.LTR,
331 width : (runGlyphsItem.advanceWidth)*(style.size)/1000,
332 height : (runGlyphsItem.bbox.maxY - runGlyphsItem.bbox.minY)*(style.size)/1000,
333 content : element[j],
334 minX : runGlyphsItem.bbox.minX,
335 maxX : runGlyphsItem.bbox.maxX,
336 minY : runGlyphsItem.bbox.minY,
337 maxY : runGlyphsItem.bbox.maxY
338 }
339
340 result.push(item);
341
342 }
343 return result;
344
345
346
347
348 }else if(element[0] == "bp"){
349
350 var beforeNewLine = await calculateTextWidthHeightAux(element[1], style);
351 if (Array.isArray(beforeNewLine)){
352 beforeNewLine = beforeNewLine.flat();
353 }
354
355 let afterNewLine = await calculateTextWidthHeightAux(element[2], style);
356 if (Array.isArray(afterNewLine)){
357 afterNewLine = afterNewLine.flat();
358 }
359
360 let breakPointNode : BreakPoint = {
361 original : beforeNewLine,
362 newLined : afterNewLine,
363 }
364
365 return breakPointNode;
366 }else if(element[0] == "hglue" && !Array.isArray(element[1])){
367 let hGlue : HGlue = {stretchFactor : parseFloat(element[1])}
368 return hGlue;
369 }
370 else{
371 return calculateTextWidthHeight(element, style);
372 }
373 }
374
375
376
377
378 /**
379 * whole document-representing class
380 */
381 export class Clo{
382 /** storing the text string into the main frame */
383 mainStream : Array<string>;
384 /** array of preprocessor functions to preprocess the `mainStream` */
385 preprocessors : Array<Function>;
386 /** the attributes for the Clo */
387 attrs: {[index: string]:any} ; // a4 size(x,y)
388
389
390 constructor(){
391 this.preprocessors = [];
392 this.mainStream = [];
393 this.attrs = {
394 "page" : A4_IN_PX, // default for a4. in px of [x, y]
395 "defaultFrameStyle" : defaultFrameStyle, // defaultFrameStyle
396 "hyphenLanguage" : 'en' // hyphenated in the language (in ISO 639)
397 };
398
399
400
401 // register the precessor functions
402 this.preprocessorRegister(splitCJKV);
403 this.preprocessorRegister(hyphenForClo);
404 this.preprocessorRegister(twoReturnsToNewline);
405 this.preprocessorRegister(spacesToBreakpoint);
406 this.preprocessorRegister(filterEmptyString);
407 }
408
409 public setAttr(attr : string, val : any):void{
410 Object.assign(this.attrs, attr, val);
411 }
412
413 public getAttr(attr:string) : any{
414 if (Object.keys(this.attrs).length === 0){
415 return this.attrs[attr];
416 }else{
417 return undefined;
418 }
419
420 }
421
422 /**
423 * register a function of preprocessor
424 * @param f a function
425 */
426 public preprocessorRegister(f : Function){
427 this.preprocessors.push(f);
428 }
429
430 public async generatePdf(){
431 // preprocessed
432 var preprocessed = this.mainStream;
433 for (var i = 0; i<this.preprocessors.length; i++){
434 preprocessed = this.preprocessors[i](preprocessed, this);
435 }
436 // generate the width and height of the stream
437
438 let defaultFontStyle : TextStyle = this.attrs["defaultFrameStyle"].textStyle;
439 let a = await calculateTextWidthHeight(preprocessed, defaultFontStyle);
440
441 // TODO
442 console.log(util.inspect(a, true, 100));
443 console.log(breakLines.totalCost(a,3,100));
444 }
445
446
447 }
448
449 /*
450 export let a = new Clo();
451 export default a; */