├── .gitignore ├── .gitmodules ├── .npmignore ├── .yarnrc.yml ├── Makefile ├── README.md ├── benchmark.js ├── entry ├── async.d.ts ├── async.js ├── common.d.ts ├── prepare.js ├── sync.d.ts └── sync.js ├── example.js ├── logo.png ├── package.json ├── sources ├── Line.hh ├── LineSizeContainer.cc ├── LineSizeContainer.hh ├── Position.hh ├── StringContainer.cc ├── StringContainer.hh ├── TextLayout.cc ├── TextLayout.hh ├── TextOperation.hh ├── Token.hh ├── TokenLocator.hh ├── embind.cc ├── run-tests.js ├── tests │ ├── catch.cc │ ├── catch.hpp │ ├── framework.hh │ ├── general.test.cc │ ├── main.test.cc │ ├── methods.test.cc │ ├── stresstests.test.cc │ └── utf8.test.cc └── tools │ ├── TextOutput.cc │ └── TextOutput.hh └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | .pnp.* 3 | node_modules 4 | *.a 5 | *.o 6 | *.d 7 | 8 | *.gypi 9 | !/final-flags.gypi 10 | 11 | /lib 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "uni-algo"] 2 | path = uni-algo 3 | url = https://github.com/uni-algo/uni-algo.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sources 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/mono-layout/4277a5d4b560a2875a10ad2bcd0b3e16d8d9e549/.yarnrc.yml -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = bash 2 | 3 | TARGET = libtformat.a 4 | 5 | SRC = $(shell find sources -name '*.cc' -not -name 'embind.cc' -not -path 'sources/tests/*') uni-algo/src/data.cpp 6 | TESTS = $(shell find sources -name '*.cc' -not -name 'embind.cc') uni-algo/src/data.cpp 7 | 8 | OBJ_SRC = $(SRC:=.o) 9 | OBJ_TESTS = $(TESTS:=.o) 10 | 11 | DEPS = $(SRC:=.d) $(TESTS:=.d) 12 | 13 | RM = rm -f 14 | CXX ?= clang++ 15 | 16 | CXXFLAGS = -std=c++17 -W -Wall -Werror -MMD -Isources/tests -Iuni-algo/include 17 | EMFLAGS = -flto --bind 18 | 19 | NODEPS = clean fclean 20 | .PHONY : all clean fclean re test 21 | 22 | EMFLAGS += \ 23 | -s WASM=1 \ 24 | -s USE_ES6_IMPORT_META=0 \ 25 | -s ASSERTIONS=0 \ 26 | -s ALLOW_MEMORY_GROWTH=1 \ 27 | -s DYNAMIC_EXECUTION=0 \ 28 | -s TEXTDECODER=0 \ 29 | -s MODULARIZE=1 \ 30 | -s ERROR_ON_UNDEFINED_SYMBOLS=0 \ 31 | -s FILESYSTEM=0 \ 32 | -s ENVIRONMENT=web \ 33 | -s MALLOC="emmalloc" \ 34 | -s INCOMING_MODULE_JS_API=['wasmBinary'] \ 35 | -s EXPORT_NAME="monoLayout" 36 | 37 | ifeq ($(DEBUG),1) 38 | CXXFLAGS += -g -O0 39 | CPPFLAGS += -DDEBUG 40 | else 41 | CXXFLAGS += -O3 42 | endif 43 | 44 | all: $(TARGET) 45 | 46 | ifeq (0, $(words $(findstring $(MAKECMDGOALS), $(NODEPS)))) 47 | -include $(DEPS) 48 | endif 49 | 50 | clean: 51 | $(RM) $(shell find . -name '*.o') 52 | $(RM) $(shell find . -name '*.d') 53 | 54 | wasm: $(SRC) 55 | mkdir -p lib 56 | em++ $(CXXFLAGS) $(CPPFLAGS) $(EMFLAGS) -s BINARYEN_ASYNC_COMPILATION=0 $(WASM_FLAGS) -o lib/mono-layout-sync.js $(SRC) sources/embind.cc 57 | em++ $(CXXFLAGS) $(CPPFLAGS) $(EMFLAGS) -s BINARYEN_ASYNC_COMPILATION=1 $(WASM_FLAGS) -o lib/mono-layout-async.js $(SRC) sources/embind.cc 58 | cp lib/mono-layout-sync.wasm lib/mono-layout.wasm 59 | rm lib/mono-layout-sync.wasm lib/mono-layout-async.wasm 60 | 61 | fclean: clean 62 | $(RM) $(TARGET) 63 | 64 | re: clean 65 | $(MAKE) all 66 | 67 | %.cc.o: %.cc 68 | ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c -o $@ $< 69 | 70 | %.cpp.o: %.cpp 71 | ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c -o $@ $< 72 | 73 | $(TARGET): $(OBJ_SRC) 74 | ar r $(TARGET) $(OBJ_SRC) 75 | ranlib $(TARGET) 76 | 77 | test: $(OBJ_TESTS) 78 | $(CXX) $(CXXFLAGS) $(CPPFLAGS) -o /tmp/testsuite $(OBJ_TESTS) 79 | /tmp/testsuite 80 | 81 | .PHONY: all clean wasm fclean re test 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Mono-Layout](/logo.png?raw=true)](https://github.com/arcanis/mono-layout) 2 | 3 | > Fast implementation of a browser-like text layout engine, for Node and browsers 4 | 5 | [![](https://img.shields.io/npm/v/mono-layout.svg)]() [![](https://img.shields.io/npm/l/mono-layout.svg)]() 6 | 7 | ## Features 8 | 9 | - Soft-wraps any text to the given width 10 | - Automatically justifies the text to fit the specified width if requested 11 | - Can collapse text on demand (`white-space: pre`, but more configurable) 12 | - Keeps an internal state to map between the original and transformed text coordinates 13 | - Only updates the part of the text that has changed for better performances 14 | - Shipped as NPM package using WebAssembly (portable, even accross browsers) 15 | - Also available as a zero-dependencies C++ library 16 | 17 | Currently not implemented: 18 | 19 | - Font width support (all characters are assumed monospaces) 20 | - Unicode support (all characters are assumed to be ASCII) 21 | 22 | ## Installation 23 | 24 | ``` 25 | $> yarn add mono-layout 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```js 31 | const {TextLayout} = require(`mono-layout/sync`); 32 | const faker = require(`faker`); 33 | 34 | const textLayout = new TextLayout(); 35 | textLayout.setConfiguration({columns: 80, justifyText: true}); 36 | textLayout.setSource(faker.lorem.paragraphs(10, `\n\n`)); 37 | 38 | console.log(textLayout.getTransformedSource()); 39 | ``` 40 | 41 | Note that the library is also available through an asynchronous endpoint (used by default when requiring `mono-layout`). You typically will want to use this endpoint if your code is expected to work within browsers, since they may disallow WebAssembly to be compiled in the main thread. Here's what the code looks like with the asynchronous initialization: 42 | 43 | ```js 44 | const tlPromise = require(`mono-layout/async`); 45 | const faker = require(`faker`); 46 | 47 | tlPromise.then(({TextLayout}) => { 48 | const textLayout = new TextLayout(); 49 | textLayout.setConfiguration({columns: 80, justifyText: true}); 50 | textLayout.setSource(faker.lorem.paragraphs(10, `\n\n`)); 51 | 52 | console.log(textLayout.getTransformedSource()); 53 | }); 54 | ``` 55 | 56 | ## Tests 57 | 58 | ### Testing the library 59 | 60 | ``` 61 | $> apt-get install catch 62 | $> make tests DEBUG=1 63 | ``` 64 | 65 | ### Testing the JS module 66 | 67 | ``` 68 | $> yarn 69 | $> node sources/run-tests.js 70 | ``` 71 | 72 | ## License (MIT) 73 | 74 | > **Copyright © 2016 Maël Nison** 75 | > 76 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 77 | > 78 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 79 | > 80 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 81 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`); 2 | const wasm = fs.readFileSync(require.resolve(`mono-layout/wasm`)); 3 | 4 | const {createContext} = require(`mono-layout/sync`); 5 | const {createLayout} = createContext(wasm); 6 | 7 | const benchmark = (description, setup, fn) => { 8 | const loopCount = 100n; 9 | let totalTime = 0n; 10 | 11 | for (let t = 0n; t < loopCount; ++t) { 12 | const props = setup(); 13 | 14 | const startTime = process.hrtime.bigint(); 15 | fn(props); 16 | const endTime = process.hrtime.bigint(); 17 | 18 | totalTime += endTime - startTime; 19 | } 20 | 21 | console.log(`${description}\n ${`${(totalTime) / loopCount}ns`.padStart(12, ` `)}`); 22 | }; 23 | 24 | const lorem = ` 25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut dignissim eros, vitae sodales erat. Vivamus placerat id ligula ultrices pharetra. Cras sodales eu turpis nec finibus. Vivamus sit amet commodo purus, non pretium lorem. Praesent eget lobortis urna. Vestibulum luctus quam eget odio egestas malesuada. Maecenas ultrices dui ligula, vitae pellentesque tortor sodales et. Nullam ut vestibulum lorem, vel sollicitudin dui. Quisque nisi nisl, blandit sed libero venenatis, lacinia auctor sem. Nulla fermentum auctor elit, sed pulvinar elit ultricies id. Phasellus dui ligula, bibendum vel tincidunt quis, tempor vel augue. 26 | 27 | In maximus metus eros, id iaculis erat consectetur vitae. Donec odio urna, auctor vel erat at, semper tempor turpis. Nullam convallis rutrum rutrum. Aenean id commodo nunc. Praesent et cursus magna, ut pharetra enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec semper ac nisl ac pharetra. 28 | 29 | Pellentesque accumsan eget eros rutrum ultrices. Quisque at sem quis justo venenatis fringilla. Maecenas hendrerit elit vitae sollicitudin posuere. Nulla facilisi. Praesent vel enim enim. Mauris ultricies dui a congue rhoncus. Nunc consectetur ipsum pretium volutpat egestas. Quisque eleifend purus urna, sed porttitor ante tincidunt a. 30 | 31 | Nullam vehicula arcu ut finibus suscipit. Vestibulum quis vehicula nisi. Aliquam placerat nibh porttitor, blandit ex id, bibendum felis. Aenean et diam quis sem tincidunt tempus quis vitae leo. Quisque sed sapien molestie, sollicitudin velit quis, auctor neque. Maecenas id scelerisque diam. Vivamus tincidunt molestie scelerisque. Vivamus interdum turpis odio, in efficitur leo sodales nec. Nunc ac nunc vehicula, euismod ex eget, dictum urna. Maecenas sed lectus tristique, ullamcorper nisi eu, iaculis turpis. Duis fermentum fermentum tellus nec facilisis. Nunc porttitor tellus id consectetur blandit. Mauris imperdiet est nunc, vitae imperdiet nunc scelerisque at. Nunc gravida sit amet massa scelerisque accumsan. 32 | 33 | Aliquam erat volutpat. Sed ac ante nibh. Proin fermentum elementum lectus et viverra. Fusce gravida quam eget libero maximus, ac fermentum purus volutpat. Quisque vulputate justo in ullamcorper luctus. Aenean vestibulum, orci eu condimentum accumsan, diam lorem consequat nibh, interdum consequat mauris lacus faucibus magna. Phasellus tempor tincidunt lorem, eget pellentesque justo. Morbi consequat, mauris id fringilla blandit, felis felis lobortis urna, ac finibus massa nibh sit amet metus. Nam vulputate mauris in luctus sollicitudin. Etiam nisl nisl, semper commodo ligula nec, cursus euismod turpis. Duis euismod nunc id sem consequat, a fermentum arcu facilisis. Duis bibendum neque non metus viverra, eget condimentum magna vestibulum. 34 | `.trim(); 35 | 36 | const loremUnicode = ` 37 | Λορεμ ιπσθμ δολορ σιτ αμετ, περ σιμθλ μολεστιε ει, qθο ιν δολορ cονσθλατθ. Ατ vελ σαεπε δεσερθντ. Νιηιλ ποστεα νο εοσ. Ειθσ αδηθc δοcτθσ vιμ νο, εαμ νε vιδιτ σολθμ γθβεργρεν. Θτ cονσθλατθ γθβεργρεν πρι. Λορεμ ιπσθμ δολορ σιτ αμετ, περ σιμθλ μολεστιε ει, qθο ιν δολορ cονσθλατθ. Ατ vελ σαεπε δεσερθντ. Νιηιλ ποστεα νο εοσ. 38 | 39 | Ατ πρι φορενσιβθσ ιντερεσσετ. Εξ περφεcτο πλατονεμ περπετθα ηισ, πραεσεντ τορqθατοσ σιγνιφερθμqθε θτ περ. Αν qθο προπριαε εξπετενδα vολθπτατθμ. Qθο πθρτο cονvενιρε θτ. Cθ vιμ νισλ πατριοqθε δεσερθισσε, θτ ηινc προβο vελ. Ιθστο σcριπτορεμ περ νε, νεc ατ δολορθμ vεριτθσ. Ατ πρι φορενσιβθσ ιντερεσσετ. 40 | 41 | Τε cασε μινιμθμ ιθδιcαβιτ ηασ, δεσερθισσε σαδιπσcινγ τε ηασ. Σιτ νοστρο ειρμοδ πραεσεντ νο. Τε εραντ ιισqθε ναμ, εαμ νο qθανδο λεγερε. Σεα διcαντ λθcιλιθσ δισσεντιασ εθ. Ωισι λθδθσ απειριαν νε vελ. Θνθμ οcθρρερετ νο μεα. Τε cασε μινιμθμ ιθδιcαβιτ ηασ, δεσερθισσε σαδιπσcινγ τε ηασ. Σιτ νοστρο ειρμοδ πραεσεντ νο. 42 | 43 | Εθμ qθοδ τραcτατοσ σcριπτορεμ εξ. Εθ δθο vοcιβθσ δετραcτο πατριοqθε. Δθισ ελιγενδι σπλενδιδε περ τε, εθμ εα ενιμ διcτα, φερρι απειριαν περ νε. Cθμ εξ σαεπε ομιτταμ, θτ μινιμθμ ιντελλεγαμ εθμ. Νοvθμ vιρισ ερθδιτι vισ εα, αδ μει γραεcι ερθδιτι θλλαμcορπερ. Εθμ qθοδ τραcτατοσ σcριπτορεμ εξ. Εθ δθο vοcιβθσ δετραcτο πατριοqθε. 44 | 45 | Ει θσθ μοvετ qθαεστιο. Vελ εα ποπθλο περσεqθερισ, τε εvερτι vολθπτθα προ. Σεδ τεμπορ ινιμιcθσ cονcεπταμ θτ, μελιθσ αλιqθανδο εθ. Ει θσθ μοvετ qθαεστιο. Vελ εα ποπθλο περσεqθερισ, τε εvερτι vολθπτθα προ. Σεδ τεμπορ ινιμιcθσ cονcεπταμ θτ, μελιθσ αλιqθανδο εθ. 46 | `.trim(); 47 | 48 | benchmark(`Layout a small ascii string`, () => { 49 | return createLayout(); 50 | }, textLayout => { 51 | textLayout.setSource(`Hello World`); 52 | }); 53 | 54 | benchmark(`Segment a large ascii text (${lorem.length} bytes) using Intl.Segmenter`, () => { 55 | return new Intl.Segmenter(`en`); 56 | }, segmenter => { 57 | [...segmenter.segment(lorem)]; 58 | }); 59 | 60 | benchmark(`Segment a large ascii text (${lorem.length} bytes) using uni-algo`, () => { 61 | return createLayout(); 62 | }, textLayout => { 63 | textLayout.setSource(lorem); 64 | }); 65 | 66 | benchmark(`Segment a large unicode text (${(new TextEncoder().encode(loremUnicode)).length} bytes) using Intl.Segmenter`, () => { 67 | return new Intl.Segmenter(`en`); 68 | }, segmenter => { 69 | [...segmenter.segment(loremUnicode)]; 70 | }); 71 | 72 | benchmark(`Segment a large unicode text (${(new TextEncoder().encode(loremUnicode)).length} bytes) using uni-algo`, () => { 73 | return createLayout(); 74 | }, textLayout => { 75 | textLayout.setSource(loremUnicode); 76 | }); 77 | -------------------------------------------------------------------------------- /entry/async.d.ts: -------------------------------------------------------------------------------- 1 | import type {Context} from './common'; 2 | 3 | export type * from './common'; 4 | 5 | export function createContext(wasm: ArrayBufferView | ArrayBuffer): Promise; 6 | -------------------------------------------------------------------------------- /entry/async.js: -------------------------------------------------------------------------------- 1 | const setupModule = require(`../lib/mono-layout-async`); 2 | 3 | const {prepare} = require(`./prepare`); 4 | 5 | exports.createContext = async wasmBinary => { 6 | const lib = prepare(await setupModule({wasmBinary})); 7 | 8 | return { 9 | createLayout() { 10 | return new lib.TextLayout(); 11 | }, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /entry/common.d.ts: -------------------------------------------------------------------------------- 1 | export type Configuration = { 2 | columns: number; 3 | tabWidth: number; 4 | 5 | softWrap: boolean; 6 | collapseWhitespaces: boolean; 7 | preserveLeadingSpaces: boolean; 8 | preserveTrailingSpaces: boolean; 9 | allowWordBreaks: boolean; 10 | demoteNewlines: boolean; 11 | justifyText: boolean; 12 | }; 13 | 14 | export type Position = { 15 | x: number; 16 | y: number; 17 | }; 18 | 19 | export type TextOperation = { 20 | startingRow: number; 21 | deletedLineCount: number; 22 | addedLineCount: number; 23 | }; 24 | 25 | export interface Context { 26 | createLayout(): TextLayout; 27 | } 28 | 29 | export interface TextLayout { 30 | getColumns(): number; 31 | getTabWidth(): number; 32 | 33 | getSoftWrap(): boolean; 34 | getCollapseWhitespaces(): boolean; 35 | getPreserveLeadingSpaces(): boolean; 36 | getPreserveTrailingSpaces(): boolean; 37 | getAllowWordBreaks(): boolean; 38 | getDemoteNewlines(): boolean; 39 | getJustifyText(): boolean; 40 | 41 | setColumns(columns: number): boolean; 42 | setTabWidth(tabWidth: number): boolean; 43 | 44 | setSoftWrap(softWrap: boolean): boolean; 45 | setCollapseWhitespaces(collapseWhitespaces: boolean): boolean; 46 | setPreserveLeadingSpaces(preserveLeadingSpaces: boolean): boolean; 47 | setPreserveTrailingSpaces(preserveTrailingSpaces: boolean): boolean; 48 | setAllowWordBreaks(allowWordBreaks: boolean): boolean; 49 | setDemoteNewlines(demoteNewlines: boolean): boolean; 50 | setJustifyText(justifyText: boolean): boolean; 51 | 52 | setConfiguration(configuration: Partial): boolean; 53 | 54 | getRowCount(): number; 55 | getColumnCount(): number; 56 | getSoftWrapCount(): number; 57 | getMaxCharacterIndex(): number; 58 | 59 | getFirstPosition(): Position; 60 | getLastPosition(): Position; 61 | 62 | doesSoftWrap(row: number): boolean; 63 | 64 | getSource(): string; 65 | getText(): string; 66 | getLine(row: number): string; 67 | getLineLength(row: number): number; 68 | getLineSlice(row: number, start: number, end: number): string; 69 | 70 | getFixedCellPosition(position: Position): Position; 71 | getFixedPosition(position: Position): Position; 72 | 73 | getPositionLeft(position: Position): [Position, boolean]; 74 | getPositionRight(position: Position): [Position, boolean]; 75 | 76 | getPositionAbove(position: Position, amplitude?: number): [Position, boolean]; 77 | getPositionBelow(position: Position, amplitude?: number): [Position, boolean]; 78 | 79 | getRowForCharacterIndex(characterIndex: number): number; 80 | getCharacterIndexForRow(row: number): number; 81 | 82 | getPositionForCharacterIndex(characterIndex: number): Position; 83 | getCharacterIndexForPosition(position: Position): number; 84 | 85 | clearSource(): TextOperation; 86 | setSource(source: string): TextOperation; 87 | spliceSource(start: number, deleted: number, added: string): TextOperation; 88 | 89 | [Symbol.iterator](): IterableIterator; 90 | } 91 | -------------------------------------------------------------------------------- /entry/prepare.js: -------------------------------------------------------------------------------- 1 | exports.prepare = lib => { 2 | lib.TextLayout.prototype.setConfiguration = function (config) { 3 | let mustUpdate = false; 4 | 5 | for (const key of Object.keys(config)) { 6 | const setter = `set${key.charAt(0).toUpperCase()}${key.substr(1)}`; 7 | 8 | if (!this[setter]) 9 | throw new Error(`Invalid configuration option "${key}"`); 10 | 11 | if (this[setter](config[key])) { 12 | mustUpdate = true; 13 | } 14 | } 15 | 16 | return mustUpdate ? this.applyConfiguration() : null; 17 | }; 18 | 19 | lib.TextLayout.prototype[Symbol.iterator] = function* () { 20 | for (let t = 0, T = this.getRowCount(); t < T; ++t) { 21 | yield this.getLine(t); 22 | } 23 | }; 24 | 25 | return lib; 26 | }; 27 | -------------------------------------------------------------------------------- /entry/sync.d.ts: -------------------------------------------------------------------------------- 1 | import type {Context} from './common'; 2 | 3 | export type * from './common'; 4 | 5 | export function createContext(wasm: ArrayBufferView | ArrayBuffer): Context; 6 | -------------------------------------------------------------------------------- /entry/sync.js: -------------------------------------------------------------------------------- 1 | const setupModule = require(`../lib/mono-layout-sync`); 2 | 3 | const {prepare} = require(`./prepare`); 4 | 5 | exports.createContext = wasmBinary => { 6 | const lib = prepare(setupModule({wasmBinary})); 7 | 8 | return { 9 | createLayout() { 10 | return new lib.TextLayout(); 11 | }, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`); 2 | const wasm = fs.readFileSync(require.resolve(`mono-layout/wasm`)); 3 | 4 | function mainSync() { 5 | const {createContext} = require(`mono-layout/sync`); 6 | 7 | const context = createContext(wasm); 8 | const layout = context.createLayout(); 9 | 10 | layout.setSource(``); 11 | } 12 | 13 | async function mainAsync() { 14 | const {createContext} = require(`mono-layout/async`); 15 | 16 | const context = await createContext(wasm); 17 | const layout = context.createLayout(); 18 | 19 | layout.setSource(``); 20 | } 21 | 22 | mainSync(); 23 | mainAsync(); 24 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcanis/mono-layout/4277a5d4b560a2875a10ad2bcd0b3e16d8d9e549/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mono-layout", 3 | "version": "0.14.4", 4 | "packageManager": "yarn@4.6.0+sha256.eaf1eeabc164a44ca0b65dbdccd54af7e55f3ff9294b3ff318d5aaec92f2b20b", 5 | "license": "MIT", 6 | "exports": { 7 | ".": "./entry/async.js", 8 | "./sync": "./entry/sync.js", 9 | "./async": "./entry/async.js", 10 | "./wasm": "./lib/mono-layout.wasm" 11 | }, 12 | "devDependencies": { 13 | "benchmark": "^2.1.4", 14 | "faker": "^4.1.0", 15 | "glob": "^7.1.1", 16 | "term-strings": "^0.14.1" 17 | }, 18 | "scripts": { 19 | "prepack": "make wasm" 20 | }, 21 | "files": [ 22 | "entry", 23 | "lib" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /sources/Line.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "./StringContainer.hh" 8 | #include "./Token.hh" 9 | 10 | struct Line { 11 | 12 | unsigned inputOffset; 13 | unsigned inputLength; 14 | 15 | unsigned outputOffset; 16 | unsigned outputLength; 17 | 18 | bool doesSoftWrap; 19 | bool hasNewline; 20 | 21 | std::vector tokens; 22 | StringContainer string; 23 | 24 | Line(void) 25 | : inputOffset(0) 26 | , inputLength(0) 27 | , outputOffset(0) 28 | , outputLength(0) 29 | , doesSoftWrap(false) 30 | , hasNewline(false) 31 | , tokens{} 32 | , string() 33 | { 34 | } 35 | 36 | Line(std::initializer_list tokens) 37 | : inputOffset(0) 38 | , inputLength(0) 39 | , outputOffset(0) 40 | , outputLength(0) 41 | , doesSoftWrap(false) 42 | , hasNewline(false) 43 | , tokens(tokens) 44 | , string() 45 | { 46 | for (auto const & token : tokens) { 47 | this->string += token.string; 48 | } 49 | } 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /sources/LineSizeContainer.cc: -------------------------------------------------------------------------------- 1 | #include "./LineSizeContainer.hh" 2 | 3 | LineSizeContainer::LineSizeContainer(void) 4 | : m_container {{ 0, 1 }} 5 | { 6 | } 7 | 8 | void LineSizeContainer::increase(unsigned size) 9 | { 10 | m_container[size] += 1; 11 | } 12 | 13 | void LineSizeContainer::decrease(unsigned size) 14 | { 15 | auto it = m_container.find(size); 16 | it->second -= 1; 17 | 18 | if (it->second == 0) { 19 | m_container.erase(it); 20 | } 21 | } 22 | 23 | unsigned LineSizeContainer::getMaxSize(void) const 24 | { 25 | return m_container.rbegin()->first; 26 | } 27 | -------------------------------------------------------------------------------- /sources/LineSizeContainer.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class LineSizeContainer { 6 | 7 | public: 8 | 9 | LineSizeContainer(void); 10 | 11 | public: 12 | 13 | void increase(unsigned size); 14 | void decrease(unsigned size); 15 | 16 | public: 17 | 18 | unsigned getMaxSize(void) const; 19 | 20 | private: 21 | 22 | std::map m_container; 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /sources/Position.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef DEBUG 4 | # include 5 | #endif 6 | 7 | struct Position { 8 | 9 | unsigned x; 10 | unsigned y; 11 | 12 | Position(void) 13 | : x(0) 14 | , y(0) 15 | { 16 | } 17 | 18 | Position(unsigned x, unsigned y) 19 | : x(x) 20 | , y(y) 21 | { 22 | } 23 | 24 | bool operator==(Position const & other) const 25 | { 26 | return x == other.x && y == other.y; 27 | } 28 | 29 | }; 30 | 31 | #ifdef DEBUG 32 | 33 | inline static std::ostream & operator <<(std::ostream & os, Position const & position) 34 | { 35 | os << "" << std::endl; 36 | 37 | return os; 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /sources/StringContainer.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "./StringContainer.hh" 5 | 6 | StringContainer & StringContainer::operator=(std::string const & value) 7 | { 8 | m_internalRepresentation.clear(); 9 | m_internalRepresentation.reserve(value.size()); 10 | 11 | for (auto c : uni::views::grapheme::utf8(value)) 12 | m_internalRepresentation.emplace_back(std::string(c)); 13 | 14 | return *this; 15 | } 16 | 17 | StringContainer & StringContainer::operator+=(StringContainer const & other) 18 | { 19 | m_internalRepresentation.reserve(m_internalRepresentation.size() + other.size()); 20 | m_internalRepresentation.insert(m_internalRepresentation.end(), other.m_internalRepresentation.begin(), other.m_internalRepresentation.end()); 21 | 22 | return *this; 23 | } 24 | 25 | StringContainer & StringContainer::operator+=(GraphemeContainer const & c) 26 | { 27 | m_internalRepresentation.emplace_back(c); 28 | 29 | return *this; 30 | } 31 | 32 | GraphemeContainer const & StringContainer::at(unsigned offset) const 33 | { 34 | return m_internalRepresentation.at(offset); 35 | } 36 | 37 | void StringContainer::splice(unsigned start, unsigned removed, std::string const & added) 38 | { 39 | std::vector addedView; 40 | addedView.reserve(added.size()); 41 | 42 | for (auto c : uni::views::grapheme::utf8(added)) 43 | addedView.emplace_back(std::string(c)); 44 | 45 | m_internalRepresentation.erase(m_internalRepresentation.begin() + start, m_internalRepresentation.begin() + start + removed); 46 | 47 | m_internalRepresentation.reserve(m_internalRepresentation.size() + addedView.size()); 48 | m_internalRepresentation.insert(m_internalRepresentation.begin() + start, addedView.begin(), addedView.end()); 49 | } 50 | 51 | size_t StringContainer::size() const 52 | { 53 | return m_internalRepresentation.size(); 54 | } 55 | 56 | StringContainer StringContainer::slice(unsigned start, unsigned end) const 57 | { 58 | StringContainer copy; 59 | copy.m_internalRepresentation.insert(copy.m_internalRepresentation.end(), m_internalRepresentation.begin() + start, m_internalRepresentation.begin() + end); 60 | 61 | return copy; 62 | } 63 | 64 | StringContainer StringContainer::slice(unsigned start) const 65 | { 66 | StringContainer copy; 67 | copy.m_internalRepresentation.insert(copy.m_internalRepresentation.end(), m_internalRepresentation.begin() + start, m_internalRepresentation.end()); 68 | 69 | return copy; 70 | } 71 | -------------------------------------------------------------------------------- /sources/StringContainer.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "uni_algo/break_grapheme.h" 7 | 8 | class GraphemeContainer { 9 | 10 | public: 11 | 12 | GraphemeContainer(std::string view) 13 | : m_view(view) 14 | { 15 | } 16 | 17 | public: 18 | 19 | size_t size() const 20 | { 21 | return m_view.size(); 22 | } 23 | 24 | char operator*() const 25 | { 26 | return m_view.at(0); 27 | } 28 | 29 | std::string view() const 30 | { 31 | return m_view; 32 | } 33 | 34 | private: 35 | 36 | std::string m_view; 37 | 38 | }; 39 | 40 | class StringContainer { 41 | 42 | public: 43 | 44 | StringContainer() 45 | : m_internalRepresentation() 46 | { 47 | } 48 | 49 | StringContainer(std::string const & str) 50 | : m_internalRepresentation() 51 | { 52 | *this = str; 53 | } 54 | 55 | public: 56 | 57 | auto begin() const 58 | { 59 | return m_internalRepresentation.begin(); 60 | } 61 | 62 | auto end() const 63 | { 64 | return m_internalRepresentation.end(); 65 | } 66 | 67 | public: 68 | 69 | StringContainer slice(unsigned start, unsigned end) const; 70 | StringContainer slice(unsigned start) const; 71 | 72 | GraphemeContainer const & at(unsigned offset) const; 73 | 74 | size_t size() const; 75 | 76 | public: 77 | 78 | StringContainer & operator=(std::string const & value); 79 | 80 | StringContainer & operator+=(StringContainer const & other); 81 | StringContainer & operator+=(GraphemeContainer const & c); 82 | 83 | void splice(unsigned start, unsigned removed, std::string const & added); 84 | 85 | std::string toString() const 86 | { 87 | std::string finalString; 88 | 89 | unsigned size = 0; 90 | for (auto & c : m_internalRepresentation) 91 | size += c.size(); 92 | 93 | finalString.reserve(size); 94 | for (auto & c : m_internalRepresentation) 95 | finalString += c.view(); 96 | 97 | return finalString; 98 | } 99 | 100 | private: 101 | 102 | std::vector m_internalRepresentation; 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /sources/TextLayout.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "./Line.hh" 8 | #include "./TextLayout.hh" 9 | #include "./TextOperation.hh" 10 | #include "./Token.hh" 11 | 12 | TextLayout::TextLayout(void) 13 | : m_columns(static_cast(-1)) 14 | , m_tabWidth(4) 15 | , m_softWrap(false) 16 | , m_collapseWhitespaces(false) 17 | , m_preserveLeadingSpaces(false) 18 | , m_preserveTrailingSpaces(false) 19 | , m_allowWordBreaks(false) 20 | , m_demoteNewlines(false) 21 | , m_justifyText(false) 22 | , m_source() 23 | , m_lineSizeContainer() 24 | , m_softWrapCount(0) 25 | , m_lines{ Line{ Token(TOKEN_DYNAMIC) } } 26 | { 27 | assert(m_lines.size() > 0); 28 | } 29 | 30 | unsigned TextLayout::getColumns(void) const 31 | { 32 | return m_columns; 33 | } 34 | 35 | unsigned TextLayout::getTabWidth(void) const 36 | { 37 | return m_tabWidth; 38 | } 39 | 40 | bool TextLayout::getSoftWrap(void) const 41 | { 42 | return m_softWrap; 43 | } 44 | 45 | bool TextLayout::getCollapseWhitespaces(void) const 46 | { 47 | return m_collapseWhitespaces; 48 | } 49 | 50 | bool TextLayout::getPreserveLeadingSpaces(void) const 51 | { 52 | return m_preserveLeadingSpaces; 53 | } 54 | 55 | bool TextLayout::getPreserveTrailingSpaces(void) const 56 | { 57 | return m_preserveTrailingSpaces; 58 | } 59 | 60 | bool TextLayout::getAllowWordBreaks(void) const 61 | { 62 | return m_allowWordBreaks; 63 | } 64 | 65 | bool TextLayout::getDemoteNewlines(void) const 66 | { 67 | return m_demoteNewlines; 68 | } 69 | 70 | #include "uni_algo/break_grapheme.h" 71 | #include "uni_algo/break_word.h" 72 | 73 | bool TextLayout::getJustifyText(void) const 74 | { 75 | return m_justifyText; 76 | } 77 | 78 | bool TextLayout::setColumns(unsigned columns) 79 | { 80 | if (m_columns == columns) 81 | return false; 82 | 83 | m_columns = columns; 84 | 85 | // When soft wrapping isn't enabled, changing the max number of columns doesn't change anything 86 | if (!m_softWrap) 87 | return false; 88 | 89 | // When soft wrapping is enabled but the number of columns is lower or equal to the new number of columns, we don't have to reset the layout if none of the lines are soft-wrapping 90 | // Note that we also need to check to make sure that getColumnCount() is higher than zero, because it's a special case that can be reached if the previous maximal number of columns is 0 91 | // Finally, we cannot apply this heuristic when the text is justified, since it might cause the layout to change 92 | if (this->getColumnCount() > 0 && this->getColumnCount() <= m_columns && this->getSoftWrapCount() == 0) 93 | return false; 94 | 95 | return true; 96 | } 97 | 98 | bool TextLayout::setTabWidth(unsigned tabWidth) 99 | { 100 | if (m_tabWidth == tabWidth) 101 | return false; 102 | 103 | m_tabWidth = tabWidth; 104 | 105 | return true; 106 | } 107 | 108 | bool TextLayout::setSoftWrap(bool softWrap) 109 | { 110 | if (m_softWrap == softWrap) 111 | return false; 112 | 113 | m_softWrap = softWrap; 114 | 115 | if (m_softWrap == true && this->getColumnCount() <= m_columns) 116 | return false; 117 | 118 | if (m_softWrap == false && this->getSoftWrapCount() == 0) 119 | return false; 120 | 121 | return true; 122 | 123 | } 124 | 125 | bool TextLayout::setCollapseWhitespaces(bool collapseWhitespaces) 126 | { 127 | if (m_collapseWhitespaces == collapseWhitespaces) 128 | return false; 129 | 130 | m_collapseWhitespaces = collapseWhitespaces; 131 | 132 | return true; 133 | } 134 | 135 | bool TextLayout::setPreserveLeadingSpaces(bool preserveLeadingSpaces) 136 | { 137 | if (m_preserveLeadingSpaces == preserveLeadingSpaces) 138 | return false; 139 | 140 | m_preserveLeadingSpaces = preserveLeadingSpaces; 141 | 142 | return true; 143 | } 144 | 145 | bool TextLayout::setPreserveTrailingSpaces(bool preserveTrailingSpaces) 146 | { 147 | if (m_preserveTrailingSpaces == preserveTrailingSpaces) 148 | return false; 149 | 150 | m_preserveTrailingSpaces = preserveTrailingSpaces; 151 | 152 | return true; 153 | } 154 | 155 | bool TextLayout::setAllowWordBreaks(bool allowWordBreaks) 156 | { 157 | if (m_allowWordBreaks == allowWordBreaks) 158 | return false; 159 | 160 | m_allowWordBreaks = allowWordBreaks; 161 | 162 | return true; 163 | } 164 | 165 | bool TextLayout::setDemoteNewlines(bool demoteNewlines) 166 | { 167 | if (m_demoteNewlines == demoteNewlines) 168 | return false; 169 | 170 | m_demoteNewlines = demoteNewlines; 171 | 172 | return true; 173 | } 174 | 175 | bool TextLayout::setJustifyText(bool justifyText) 176 | { 177 | if (m_justifyText == justifyText) 178 | return false; 179 | 180 | m_justifyText = justifyText; 181 | 182 | return true; 183 | } 184 | 185 | unsigned TextLayout::getRowCount(void) const 186 | { 187 | return m_lines.size(); 188 | } 189 | 190 | unsigned TextLayout::getColumnCount(void) const 191 | { 192 | return m_lineSizeContainer.getMaxSize(); 193 | } 194 | 195 | unsigned TextLayout::getSoftWrapCount(void) const 196 | { 197 | return m_softWrapCount; 198 | } 199 | 200 | unsigned TextLayout::getMaxCharacterIndex(void) const 201 | { 202 | return m_source.size(); 203 | } 204 | 205 | Position TextLayout::getFirstPosition(void) const 206 | { 207 | return Position(0, 0); 208 | } 209 | 210 | Position TextLayout::getLastPosition(void) const 211 | { 212 | return Position(m_lines.back().outputLength, m_lines.size() - 1); 213 | } 214 | 215 | bool TextLayout::doesSoftWrap(unsigned row) const 216 | { 217 | assert(row < m_lines.size()); 218 | 219 | return m_lines.at(row).doesSoftWrap; 220 | } 221 | 222 | std::string TextLayout::getSource(void) const 223 | { 224 | return m_source.toString(); 225 | } 226 | 227 | std::string TextLayout::getText(void) const 228 | { 229 | std::string str = m_lines.front().string.toString(); 230 | 231 | for (unsigned t = 1; t < m_lines.size(); ++t) { 232 | str += "\n"; 233 | str += m_lines.at(t).string.toString(); 234 | } 235 | 236 | return str; 237 | } 238 | 239 | std::string TextLayout::getLine(unsigned row) const 240 | { 241 | assert(row < m_lines.size()); 242 | 243 | return m_lines.at(row).string.toString(); 244 | } 245 | 246 | unsigned TextLayout::getLineLength(unsigned row) const 247 | { 248 | assert(row < m_lines.size()); 249 | 250 | return m_lines.at(row).string.size(); 251 | } 252 | 253 | std::string TextLayout::getLineSlice(unsigned row, unsigned start, unsigned end) const 254 | { 255 | assert(row < m_lines.size()); 256 | 257 | return m_lines.at(row).string.slice(start, end).toString(); 258 | } 259 | 260 | TokenLocator TextLayout::findTokenLocatorForPosition(Position const & position) const 261 | { 262 | assert(position.y < m_lines.size()); 263 | 264 | Line const & line = m_lines.at(position.y); 265 | 266 | assert(position.x <= line.outputLength); 267 | 268 | auto tokenIterator = std::lower_bound(line.tokens.begin(), line.tokens.end(), position.x, [] (auto const & token, auto x) { 269 | return token.outputOffset + token.outputLength <= x; 270 | }); 271 | 272 | if (tokenIterator == line.tokens.end()) 273 | tokenIterator -= 1; 274 | 275 | unsigned tokenIndex = tokenIterator - line.tokens.begin(); 276 | Token const & token = *tokenIterator; 277 | 278 | return TokenLocator(position.y, tokenIndex, line, token); 279 | } 280 | 281 | Position TextLayout::getFixedCellPosition(Position position) const 282 | { 283 | assert(m_lines.size() > 0); 284 | 285 | position.y = std::min(position.y, static_cast(m_lines.size() - 1)); 286 | position.x = std::min(position.x, m_lines.at(position.y).outputLength); 287 | 288 | return position; 289 | } 290 | 291 | Position TextLayout::getFixedPosition(Position position) const 292 | { 293 | position = this->getFixedCellPosition(position); 294 | 295 | auto const & line = m_lines.at(position.y); 296 | 297 | // if we're on the left or right edges of the line, everything's fine 298 | if (position.x == 0 || position.x == line.outputLength) 299 | return position; 300 | 301 | auto const & token = this->findTokenLocatorForPosition(position).token; 302 | 303 | if (token.canBeSubdivided) { 304 | 305 | // if we're closer to the left side, we jump to it 306 | if (position.x <= token.outputOffset + token.outputLength / 2) { 307 | position.x = token.outputOffset; 308 | 309 | // and if we're closer to right side, same old same old 310 | } else { 311 | position.x = token.outputOffset + token.outputLength; 312 | } 313 | 314 | } 315 | 316 | return position; 317 | } 318 | 319 | std::pair TextLayout::getPositionLeft(Position position) const 320 | { 321 | assert(m_lines.size() > 0); 322 | assert(position.y < m_lines.size()); 323 | 324 | auto tokenLocator = this->findTokenLocatorForPosition(position); 325 | 326 | auto const & line = tokenLocator.line; 327 | auto const & token = tokenLocator.token; 328 | 329 | // if we're inside the token or on its right edge 330 | if (position.x > token.outputOffset) { 331 | 332 | // if our token can be subdivided, we just move inside 333 | if (token.canBeSubdivided) { 334 | position.x -= 1; 335 | 336 | // otherwise, we need to teleport ourselves to the beginning of the token 337 | } else { 338 | position.x = token.outputOffset; 339 | } 340 | 341 | // if we're on the left edge of the token 342 | } else { 343 | 344 | // if we're on the left edge of the first token of the line, we'll need to move to the end of the line above if possible 345 | if (tokenLocator.tokenIndex == 0) { 346 | 347 | // but only if possible :) otherwise, we don't do anything 348 | if (position.y > 0) { 349 | position.y -= 1; 350 | position.x = m_lines.at(position.y).outputLength; 351 | } 352 | 353 | // otherwise, try to move inside its left neighbor 354 | } else { 355 | 356 | auto const & neighbor = line.tokens.at(tokenLocator.tokenIndex - 1); 357 | 358 | // if we can enter inside our left neighbor, we just do it 359 | if (neighbor.canBeSubdivided) { 360 | position.x -= 1; 361 | 362 | // otherwise, we just simply jump to its beginning 363 | } else { 364 | position.x = neighbor.outputOffset; 365 | } 366 | 367 | } 368 | 369 | } 370 | 371 | return { position, true }; 372 | } 373 | 374 | std::pair TextLayout::getPositionRight(Position position) const 375 | { 376 | assert(m_lines.size() > 0); 377 | assert(position.y < m_lines.size()); 378 | 379 | auto tokenLocator = this->findTokenLocatorForPosition(position); 380 | 381 | auto const & line = tokenLocator.line; 382 | auto const & token = tokenLocator.token; 383 | 384 | // if we're inside the token or on its left edge 385 | if (position.x < token.outputOffset + token.outputLength) { 386 | 387 | // if our token can be subdivided, we just move inside 388 | if (token.canBeSubdivided) { 389 | position.x += 1; 390 | 391 | // otherwise, we need to teleport ourselves to the end of the token 392 | } else { 393 | position.x = token.outputOffset + token.outputLength; 394 | } 395 | 396 | // if we're on the right edge of the token 397 | } else { 398 | 399 | // if we're on the right edge of the last token of the line, we'll need to move to the beginning of the line below if possible 400 | if (tokenLocator.tokenIndex == line.tokens.size() - 1) { 401 | 402 | // but only if possible :) otherwise, we don't do anything 403 | if (position.y < m_lines.size() - 1) { 404 | position.y += 1; 405 | position.x = 0; 406 | } 407 | 408 | // otherwise, try to move inside its right neighbor 409 | } else { 410 | 411 | auto const & neighbor = line.tokens.at(tokenLocator.tokenIndex + 1); 412 | 413 | // if we can enter inside our right neighbor, we just do it 414 | if (neighbor.canBeSubdivided) { 415 | position.x += 1; 416 | 417 | // otherwise, we just simply jump to its end 418 | } else { 419 | position.x = neighbor.outputOffset + neighbor.outputLength; 420 | } 421 | 422 | } 423 | 424 | } 425 | 426 | return { position, true }; 427 | } 428 | 429 | std::pair TextLayout::getPositionAbove(Position position) const 430 | { 431 | return this->getPositionAbove(position, 1); 432 | } 433 | 434 | std::pair TextLayout::getPositionAbove(Position position, unsigned amplitude) const 435 | { 436 | assert(m_lines.size() > 0); 437 | assert(position.y < m_lines.size()); 438 | 439 | bool perfectFit = true; 440 | 441 | if (amplitude == 0) 442 | return { position, perfectFit }; 443 | 444 | // if jumping with the requested amplitude would bring above the very first line, we just go to the beginning of the line (/!\ Careful to underflows /!\) 445 | if (amplitude > position.y) { 446 | 447 | position.y = 0; 448 | position.x = 0; 449 | 450 | } else { 451 | position.y -= amplitude; 452 | 453 | // if we land on the left edge of a line, short-circuit the rest of the procedure, since it will always be a valid positioning 454 | if (position.x == 0) { 455 | 456 | return { position, perfectFit }; 457 | 458 | // if we land on the right edge of the line or beyond, same old same old, we can short-circuit the rest of the procedure as long as we stay on the line 459 | } else if (position.x >= m_lines.at(position.y).outputLength) { 460 | 461 | perfectFit = position.x == m_lines.at(position.y).outputLength; 462 | position.x = m_lines.at(position.y).outputLength; 463 | 464 | return { position, perfectFit }; 465 | 466 | // otherwise, we have to check the token on which we land, to make sure we stay outside if it can't be subdivided 467 | } else { 468 | 469 | auto tokenLocator = this->findTokenLocatorForPosition(position); 470 | auto const & token = tokenLocator.token; 471 | 472 | if (!token.canBeSubdivided) { 473 | 474 | // if we're closer to the left side, we jump to it 475 | if (position.x <= token.outputOffset + token.outputLength / 2) { 476 | position.x = token.outputOffset; 477 | 478 | // and if we're closer to right side, same old same old 479 | } else { 480 | position.x = token.outputOffset + token.outputLength; 481 | } 482 | 483 | } 484 | 485 | } 486 | 487 | } 488 | 489 | return { position, perfectFit }; 490 | } 491 | 492 | std::pair TextLayout::getPositionBelow(Position position) const 493 | { 494 | return this->getPositionBelow(position, 1); 495 | } 496 | 497 | std::pair TextLayout::getPositionBelow(Position position, unsigned amplitude) const 498 | { 499 | assert(m_lines.size() > 0); 500 | assert(position.y < m_lines.size()); 501 | 502 | bool perfectFit = true; 503 | 504 | if (amplitude == 0) 505 | return { position, perfectFit }; 506 | 507 | // if jumping with the requested amplitude would bring below the very last line, we just go to the end of the line (/!\ Careful to underflows /!\) 508 | if (amplitude > m_lines.size() - position.y - 1) { 509 | 510 | position.y = m_lines.size() - 1; 511 | position.x = m_lines.at(position.y).outputLength; 512 | 513 | } else { 514 | 515 | position.y += amplitude; 516 | 517 | // if we land on the left edge of a line, short-circuit the rest of the procedure, since it will always be a valid positioning 518 | if (position.x == 0) { 519 | 520 | return { position, perfectFit }; 521 | 522 | // if we land on the right edge of the line or beyond, same old same old, we can short-circuit the rest of the procedure as long as we stay on the line 523 | } else if (position.x >= m_lines.at(position.y).outputLength) { 524 | 525 | perfectFit = position.x == m_lines.at(position.y).outputLength; 526 | position.x = m_lines.at(position.y).outputLength; 527 | 528 | return { position, perfectFit }; 529 | 530 | // otherwise, we have to check the token on which we land, to make sure we stay outside if it can't be subdivided 531 | } else { 532 | 533 | auto tokenLocator = this->findTokenLocatorForPosition(position); 534 | auto const & token = tokenLocator.token; 535 | 536 | if (!token.canBeSubdivided) { 537 | 538 | // if we're closer to the left side, we jump to it 539 | if (position.x <= token.outputOffset + token.outputLength / 2) { 540 | position.x = token.outputOffset; 541 | 542 | // and if we're closer to right side, same old same old 543 | } else { 544 | position.x = token.outputOffset + token.outputLength; 545 | } 546 | 547 | } 548 | 549 | } 550 | 551 | } 552 | 553 | return { position, perfectFit }; 554 | } 555 | 556 | unsigned TextLayout::getRowForCharacterIndex(unsigned characterIndex) const 557 | { 558 | assert(m_lines.size() > 0); 559 | assert(characterIndex <= m_lines.back().inputOffset + m_lines.back().inputLength); 560 | 561 | auto lineIterator = std::lower_bound(m_lines.begin(), m_lines.end(), characterIndex, [] (auto const & line, auto characterIndex) { 562 | return line.inputOffset + line.inputLength <= characterIndex; 563 | }); 564 | 565 | if (lineIterator != m_lines.end()) { 566 | return static_cast(lineIterator - m_lines.begin()); 567 | } else { 568 | return static_cast(lineIterator - m_lines.begin() - 1); 569 | } 570 | } 571 | 572 | unsigned TextLayout::getCharacterIndexForRow(unsigned row) const 573 | { 574 | assert(row < m_lines.size()); 575 | 576 | return m_lines.at(row).inputOffset; 577 | } 578 | 579 | Position TextLayout::getPositionForCharacterIndex(unsigned characterIndex) const 580 | { 581 | assert(m_lines.size() > 0); 582 | assert(characterIndex <= m_lines.back().inputOffset + m_lines.back().inputLength); 583 | 584 | Position position; 585 | 586 | auto lineIterator = std::lower_bound(m_lines.begin(), m_lines.end(), characterIndex, [] (auto const & line, auto characterIndex) { 587 | return line.inputOffset + line.inputLength <= characterIndex; 588 | }); 589 | 590 | if (lineIterator == m_lines.end()) { 591 | 592 | position.y = m_lines.size() - 1; 593 | position.x = m_lines.at(position.y).outputLength; 594 | 595 | } else { 596 | 597 | position.y = lineIterator - m_lines.begin(); 598 | 599 | auto tokenIterator = std::lower_bound(lineIterator->tokens.begin(), lineIterator->tokens.end(), characterIndex - lineIterator->inputOffset, [] (auto const & token, auto characterIndex) { 600 | return token.inputOffset + token.inputLength <= characterIndex; 601 | }); 602 | 603 | if (tokenIterator->canBeSubdivided) { 604 | position.x = tokenIterator->outputOffset + (characterIndex - lineIterator->inputOffset - tokenIterator->inputOffset); 605 | } else if (tokenIterator->inputOffset + tokenIterator->inputLength == characterIndex - lineIterator->inputOffset) { 606 | position.x = tokenIterator->outputOffset + tokenIterator->outputLength; 607 | } else { 608 | position.x = tokenIterator->outputOffset; 609 | } 610 | 611 | } 612 | 613 | return position; 614 | } 615 | 616 | unsigned TextLayout::getCharacterIndexForPosition(Position position) const 617 | { 618 | assert(m_lines.size() > 0); 619 | assert(position.y < m_lines.size()); 620 | 621 | auto tokenLocator = this->findTokenLocatorForPosition(position); 622 | 623 | auto const & line = tokenLocator.line; 624 | auto const & token = tokenLocator.token; 625 | 626 | // if the character is located on the left edge of the token, everything's fine 627 | if (position.x == token.outputOffset) { 628 | 629 | return line.inputOffset + token.inputOffset; 630 | 631 | // same if we're on the right edge of a token 632 | } else if (position.x == token.outputOffset + token.outputLength) { 633 | 634 | return line.inputOffset + token.inputOffset + token.inputLength; 635 | 636 | // if we reach this case, it means that the character is located inside a token 637 | } else { 638 | 639 | // we can subdivise static tokens, but we cannot do this for dynamic tokens 640 | assert(token.canBeSubdivided); 641 | 642 | return line.inputOffset + token.inputOffset + position.x - token.outputOffset; 643 | 644 | } 645 | } 646 | 647 | TextOperation TextLayout::applyConfiguration(void) 648 | { 649 | assert(m_lines.size() > 0); 650 | 651 | return this->update(0, m_source.size(), m_source.size()); 652 | } 653 | 654 | TextOperation TextLayout::clearSource(void) 655 | { 656 | return this->setSource(""); 657 | } 658 | 659 | TextOperation TextLayout::setSource(std::string const & source) 660 | { 661 | assert(m_lines.size() > 0); 662 | 663 | auto beforeSize = m_source.size(); 664 | m_source = source; 665 | auto afterSize = m_source.size(); 666 | 667 | return this->update(0, beforeSize, afterSize); 668 | } 669 | 670 | 671 | TextOperation TextLayout::spliceSource(unsigned start, unsigned removed, std::string const & added) 672 | { 673 | auto beforeSize = m_source.size(); 674 | m_source.splice(start, removed, added); 675 | auto afterSize = m_source.size(); 676 | 677 | auto addedSize = removed + afterSize - beforeSize; 678 | return this->update(start, removed, addedSize); 679 | } 680 | 681 | TextOperation TextLayout::update(unsigned start, unsigned removed, unsigned added) 682 | { 683 | auto errorCharacter = GraphemeContainer("?"); 684 | auto spaceCharacter = GraphemeContainer(" "); 685 | 686 | #define GET_CHARACTER_COUNT() m_source.size() 687 | #define GET_CHARACTER(OFFSET) m_source.at(OFFSET) 688 | 689 | #define SET_OFFSET(OFFSET) do { offset = (OFFSET); offsetChar = offset < offsetMax ? GET_CHARACTER(offset) : errorCharacter; } while (0) 690 | 691 | #define IS_NEWLINE() (!IS_END_OF_FILE() && !m_demoteNewlines && (*offsetChar == '\r' || *offsetChar == '\n')) 692 | #define IS_WHITESPACE() (!IS_END_OF_FILE() && (*offsetChar == ' ' || *offsetChar == '\t' || (m_demoteNewlines && (*offsetChar == '\r' || *offsetChar == '\n')))) 693 | #define IS_WORD() (!IS_END_OF_FILE() && !IS_WHITESPACE() && !IS_NEWLINE()) 694 | 695 | #define SHIFT_CHARACTER() ({ auto c = offsetChar; SET_OFFSET(offset + 1); c; }) 696 | #define SHIFT_WHILE(COND, MAX) ({ StringContainer output; while (output.size() < MAX && COND) output += SHIFT_CHARACTER(); output; }) 697 | 698 | #define SHIFT_WHITESPACES() SHIFT_WHILE(IS_WHITESPACE(), static_cast(-1)) 699 | #define SHIFT_WORD() SHIFT_WHILE(IS_WORD(), static_cast(-1)) 700 | 701 | #define SHIFT_WHITESPACES_UNTIL(MAX) SHIFT_WHILE(IS_WHITESPACE(), MAX) 702 | #define SHIFT_WORD_UNTIL(MAX) SHIFT_WHILE(IS_WORD(), MAX) 703 | 704 | #define IS_END_OF_LINE() (IS_NEWLINE() || IS_END_OF_FILE()) 705 | #define IS_END_OF_FILE() (offset >= offsetMax) 706 | 707 | #define NEW_TOKEN(TYPE) ({ Token token = Token(TYPE); token.inputOffset = currentLine.inputLength; token.outputOffset = currentLine.outputLength; token; }) 708 | #define PUSH_TOKEN(TOKEN) do { Token const & _token = (TOKEN); currentLine.tokens.push_back(_token); currentLine.inputLength += _token.inputLength; currentLine.outputLength += _token.outputLength; } while (0) 709 | 710 | // Create a structure that we will use to return a proper layout update (startingRow, removedLineCount & addedLines) 711 | TextOperation textOperation; 712 | 713 | // We only care about the number of columns if the soft wrap feature is enabled 714 | auto effectiveColumns = m_softWrap ? m_columns : std::numeric_limits::max(); 715 | 716 | // We only allow soft wrapping if the soft wrap feature is also enabled 717 | auto effectiveJustifyText = m_softWrap && m_justifyText; 718 | 719 | // Compute the rows that surround the removed line segment 720 | auto rowStart = this->getRowForCharacterIndex(start); 721 | auto rowEnd = this->getRowForCharacterIndex(start + removed) + 1; 722 | 723 | // Use the starting row to round the starting offset to the beginning of the line 724 | auto offsetStart = this->getCharacterIndexForRow(rowStart), offset = offsetStart; 725 | 726 | // Create a temporary local buffer, so that we don't need to call getCharacter() & getCharacterCount() more than needed (which might get expensive, especially when crossing asmjs boundaries). 727 | auto offsetMax = GET_CHARACTER_COUNT(); 728 | auto offsetChar = offset < offsetMax ? GET_CHARACTER(offset) : errorCharacter; 729 | 730 | // Also compute a tentative position where to stop the formatting process. It will be increased later if our newly generated lines invalidate their successors. 731 | // the +1 is required so that we actually iterate even when the very last line is empty (for example with "Foobar\n" or even "" - without this extra increment, we wouldn't create the very last empty line) 732 | auto offsetEnd = offsetMax + 1; 733 | 734 | // Compute the starting point of our modifications 735 | textOperation.startingRow = rowStart; 736 | 737 | // Compute the number of rows that we know have been deleted (this number might be increased later if newly generated rows invalidate their successors) 738 | textOperation.deletedLineCount = rowEnd - rowStart; 739 | 740 | // We start with zero added lines 741 | textOperation.addedLineCount = 0; 742 | 743 | // Check that the configuration isn't weird, and then start looping over each character 744 | if (effectiveColumns == 0) { 745 | 746 | textOperation.addedLineCount += 1; 747 | textOperation.addedLines.push_back(Line{ Token(TOKEN_DYNAMIC) }); 748 | textOperation.addedLines.back().inputLength = GET_CHARACTER_COUNT(); 749 | 750 | } else while (offset < offsetEnd) { 751 | 752 | // Create a new line that will then be populated with new tokens 753 | Line currentLine = Line(); 754 | 755 | // Find its predecessor 756 | Line const * previousLine = nullptr; 757 | 758 | if (textOperation.addedLineCount > 0) 759 | previousLine = &(textOperation.addedLines.back()); 760 | else if (rowStart > 0) 761 | previousLine = &(m_lines.at(rowStart - 1)); 762 | 763 | // Compute the location of the line inside the input string 764 | currentLine.inputOffset = offset; 765 | 766 | // Compute the location of the line inside the output string 767 | currentLine.outputOffset = previousLine ? previousLine->outputOffset + previousLine->outputLength : 0; 768 | 769 | // Check if the first characters are whitespaces and if they need to be removed (only if we're on the start of a new hard-line, or on the start of a soft-line but only when collapsing whitespaces) 770 | if (IS_WHITESPACE() && (!m_preserveLeadingSpaces || (m_collapseWhitespaces && previousLine && !previousLine->doesSoftWrap))) { 771 | 772 | Token token = Token(TOKEN_DYNAMIC); 773 | 774 | token.inputOffset = currentLine.inputLength; 775 | token.outputOffset = currentLine.outputLength; 776 | 777 | SHIFT_WHITESPACES(); 778 | 779 | token.inputLength = offset - token.inputOffset - currentLine.inputOffset; 780 | token.outputLength = 0; 781 | 782 | PUSH_TOKEN(token); 783 | 784 | } 785 | 786 | // Iterate to find enough tokens to fill the line (until the requested number of columns is reached, or until the next \n, whatever happens first) 787 | while (!IS_END_OF_LINE()) { 788 | 789 | if (currentLine.outputLength < effectiveColumns && IS_WHITESPACE()) { 790 | 791 | // Add a single space character when collapsing spaces 792 | if (m_collapseWhitespaces) { 793 | 794 | Token token = Token(TOKEN_WHITESPACES); 795 | 796 | token.inputOffset = currentLine.inputLength; 797 | token.outputOffset = currentLine.outputLength; 798 | 799 | SHIFT_WHITESPACES(); 800 | 801 | token.inputLength = offset - token.inputOffset - currentLine.inputOffset; 802 | token.outputLength = 1; 803 | 804 | token.string = " "; 805 | 806 | PUSH_TOKEN(token); 807 | 808 | // Otherwise, add all those spaces but normalize them first 809 | } else { 810 | 811 | Token token = NEW_TOKEN(TOKEN_WHITESPACES); 812 | token.canBeSubdivided = true; 813 | 814 | for (auto c : SHIFT_WHITESPACES_UNTIL(effectiveColumns - currentLine.outputLength)) switch (*c) { 815 | 816 | case '\r': 817 | case '\n': 818 | case ' ': { 819 | 820 | token.string += spaceCharacter; 821 | 822 | token.inputLength += 1; 823 | token.outputLength += 1; 824 | 825 | } break; 826 | 827 | case '\t': { 828 | 829 | if (token.inputLength > 0 || token.outputLength > 0) { 830 | 831 | PUSH_TOKEN(token); 832 | 833 | token = NEW_TOKEN(TOKEN_WHITESPACES); 834 | token.canBeSubdivided = false; 835 | 836 | } else { 837 | 838 | token.canBeSubdivided = false; 839 | 840 | } 841 | 842 | token.string += GraphemeContainer(std::string(m_tabWidth, ' ')); 843 | 844 | token.inputLength += 1; 845 | token.outputLength += m_tabWidth; 846 | 847 | PUSH_TOKEN(token); 848 | 849 | token = NEW_TOKEN(TOKEN_WHITESPACES); 850 | token.canBeSubdivided = true; 851 | 852 | } break; 853 | 854 | } 855 | 856 | if (token.inputLength > 0 || token.outputLength > 0) { 857 | PUSH_TOKEN(token); 858 | } 859 | 860 | } 861 | 862 | assert(currentLine.outputLength <= effectiveColumns); 863 | 864 | } // end of IS_WHITESPACE check 865 | 866 | if (currentLine.outputLength < effectiveColumns && IS_WORD()) { 867 | 868 | if (m_allowWordBreaks) { 869 | 870 | Token token = Token(TOKEN_WORD); 871 | 872 | token.canBeSubdivided = true; 873 | 874 | token.inputOffset = currentLine.inputLength; 875 | token.outputOffset = currentLine.outputLength; 876 | 877 | token.string = SHIFT_WORD_UNTIL(effectiveColumns - currentLine.outputLength); 878 | 879 | token.inputLength = offset - token.inputOffset - currentLine.inputOffset; 880 | token.outputLength = token.string.size(); 881 | 882 | PUSH_TOKEN(token); 883 | 884 | } else { 885 | 886 | Token token = Token(TOKEN_WORD); 887 | 888 | token.canBeSubdivided = true; 889 | 890 | token.inputOffset = currentLine.inputLength; 891 | token.outputOffset = currentLine.outputLength; 892 | 893 | token.string = SHIFT_WORD_UNTIL(effectiveColumns - currentLine.outputLength); 894 | 895 | if (!IS_WORD() || currentLine.outputLength == 0) { 896 | 897 | token.inputLength = offset - token.inputOffset - currentLine.inputOffset; 898 | token.outputLength = token.string.size(); 899 | 900 | PUSH_TOKEN(token); 901 | 902 | } else { 903 | 904 | SET_OFFSET(currentLine.inputOffset + token.inputOffset); 905 | 906 | // we need to break manually, otherwise we won't ever leave this loop since the offset will never change 907 | break; 908 | 909 | } 910 | 911 | } 912 | 913 | assert(currentLine.outputLength <= effectiveColumns); 914 | 915 | } // end of IS_WORD check 916 | 917 | if (currentLine.outputLength == effectiveColumns) { 918 | break; 919 | } 920 | 921 | } // end of line tokens loop 922 | 923 | assert(offset >= currentLine.inputOffset + currentLine.inputLength); 924 | 925 | currentLine.doesSoftWrap = !IS_END_OF_LINE(); 926 | 927 | auto hasNewline = IS_NEWLINE(); 928 | 929 | if (hasNewline) 930 | SHIFT_CHARACTER(); 931 | 932 | // if we don't care about trailing and/or if we need to collapse our whitespaces and we're on a soft-wrapping line 933 | if (!m_preserveTrailingSpaces || ((m_collapseWhitespaces || effectiveJustifyText) && currentLine.doesSoftWrap && !IS_END_OF_FILE())) { 934 | 935 | auto tokenIterator = currentLine.tokens.rbegin(); 936 | 937 | while (tokenIterator != currentLine.tokens.rend() && tokenIterator->type == TOKEN_WHITESPACES) 938 | tokenIterator += 1; 939 | 940 | // if tokenIterator goes to rend(), it means that the whole string is full of whitespaces. If the whole string is full of whitespaces, it means that we're on the first string and that we've already determined that these spaces should not be removed, so we skip this procedure. We also skip it if there's no trailing whitespaces, of course. 941 | if (tokenIterator != currentLine.tokens.rbegin() && tokenIterator != currentLine.tokens.rend()) { 942 | 943 | // Fix the line properties to account for the soon-to-be-removed tokens 944 | currentLine.inputLength = tokenIterator->inputOffset + tokenIterator->inputLength; 945 | currentLine.outputLength = tokenIterator->outputOffset + tokenIterator->outputLength; 946 | 947 | // Remove the extraneous tokens from the line 948 | auto extraTokenCount = tokenIterator - currentLine.tokens.rbegin(); 949 | currentLine.tokens.resize(currentLine.tokens.size() - extraTokenCount); 950 | 951 | } 952 | 953 | } 954 | 955 | if (offset > currentLine.inputOffset + currentLine.inputLength || currentLine.tokens.size() == 0) { 956 | 957 | Token token = Token(TOKEN_DYNAMIC); 958 | 959 | token.inputOffset = currentLine.inputLength; 960 | token.outputOffset = currentLine.outputLength; 961 | 962 | token.string = ""; 963 | 964 | token.inputLength = offset - token.inputOffset - currentLine.inputOffset; 965 | token.outputLength = 0; 966 | 967 | PUSH_TOKEN(token); 968 | 969 | } 970 | 971 | // Try to justify the line if needed and requested, except on the last soft-line of the hard-line, if that makes sense 972 | if (effectiveJustifyText && !IS_END_OF_LINE() && currentLine.outputLength < effectiveColumns) { 973 | 974 | // Hold the number of spaces we've added so far 975 | auto extraSpaceCount = 0u; 976 | 977 | // Hold the number of spaces that we still need to add 978 | auto missingSpaceCount = effectiveColumns - currentLine.outputLength; 979 | 980 | // Hold the number of slots where we can actually fit those extra spaces 981 | auto availableSlotCount = static_cast(std::count_if(currentLine.tokens.begin(), currentLine.tokens.end(), [](auto const & token) { 982 | return token.type == TOKEN_WHITESPACES && token.outputOffset > 0; 983 | })); 984 | 985 | if (availableSlotCount > 0) { 986 | 987 | // Hold the number of spaces we will have to add on each slot (=> ceil(missingSpaceCount / availableSlotCount)) 988 | auto spacesPerSlot = 1u + ((missingSpaceCount - 1u) / availableSlotCount); 989 | 990 | for (auto & token : currentLine.tokens) { 991 | 992 | token.outputOffset += extraSpaceCount; 993 | 994 | if (missingSpaceCount > 0 && token.type == TOKEN_WHITESPACES && token.outputOffset > 0) { 995 | 996 | auto localSpaceCount = std::min(spacesPerSlot, missingSpaceCount); 997 | 998 | token.canBeSubdivided = false; 999 | 1000 | token.outputLength += localSpaceCount; 1001 | token.string += GraphemeContainer(std::string(localSpaceCount, ' ')); 1002 | 1003 | extraSpaceCount += localSpaceCount; 1004 | missingSpaceCount -= localSpaceCount; 1005 | 1006 | currentLine.outputLength += localSpaceCount; 1007 | 1008 | } 1009 | 1010 | } 1011 | 1012 | } 1013 | 1014 | } 1015 | 1016 | assert(currentLine.tokens.size() > 0); 1017 | 1018 | for (auto & token : currentLine.tokens) 1019 | currentLine.string += token.string; 1020 | 1021 | textOperation.addedLineCount += 1; 1022 | textOperation.addedLines.push_back(currentLine); 1023 | 1024 | while (rowStart + textOperation.deletedLineCount != m_lines.size() && m_lines.at(rowStart + textOperation.deletedLineCount).inputOffset + added < offset + removed) 1025 | textOperation.deletedLineCount += 1; 1026 | 1027 | if (rowStart + textOperation.deletedLineCount != m_lines.size() && m_lines.at(rowStart + textOperation.deletedLineCount).inputOffset + added == offset + removed) 1028 | break; 1029 | 1030 | if (!hasNewline && IS_END_OF_FILE()) { 1031 | break; 1032 | } 1033 | 1034 | } // end of line loop 1035 | 1036 | this->apply(textOperation); 1037 | 1038 | return textOperation; 1039 | } 1040 | 1041 | void TextLayout::apply(TextOperation const & textOperation) 1042 | { 1043 | for (unsigned t = 0u; t < textOperation.deletedLineCount; ++t) { 1044 | 1045 | Line const & line = m_lines.at(textOperation.startingRow + t); 1046 | 1047 | m_lineSizeContainer.decrease(line.outputLength); 1048 | 1049 | if (line.doesSoftWrap) { 1050 | m_softWrapCount -= 1; 1051 | } 1052 | 1053 | } 1054 | 1055 | m_lines.erase(m_lines.begin() + textOperation.startingRow, m_lines.begin() + textOperation.startingRow + textOperation.deletedLineCount); 1056 | m_lines.insert(m_lines.begin() + textOperation.startingRow, textOperation.addedLines.begin(), textOperation.addedLines.end()); 1057 | 1058 | assert(m_lines.size() > 0); 1059 | 1060 | for (unsigned t = 0u; t < textOperation.addedLineCount; ++t) { 1061 | 1062 | Line const & line = m_lines.at(textOperation.startingRow + t); 1063 | 1064 | m_lineSizeContainer.increase(line.outputLength); 1065 | 1066 | if (line.doesSoftWrap) { 1067 | m_softWrapCount += 1; 1068 | } 1069 | 1070 | } 1071 | 1072 | for (unsigned t = std::max(1u, static_cast(textOperation.startingRow + textOperation.addedLineCount)); t < m_lines.size(); ++t) { 1073 | m_lines.at(t).inputOffset = m_lines.at(t - 1).inputOffset + m_lines.at(t - 1).inputLength; 1074 | m_lines.at(t).outputOffset = m_lines.at(t - 1).outputOffset + m_lines.at(t - 1).outputLength; 1075 | } 1076 | } 1077 | 1078 | #ifdef DEBUG 1079 | 1080 | #include 1081 | 1082 | void TextLayout::dump(std::vector const & lines) const 1083 | { 1084 | std::cout << "========================================================" << std::endl; 1085 | std::cout << "CONFIGURATION" << std::endl; 1086 | std::cout << "========================================================" << std::endl; 1087 | std::cout << std::endl; 1088 | std::cout << " Soft Wrap: " << m_softWrap << std::endl; 1089 | std::cout << " Collapse Whitespaces: " << m_collapseWhitespaces << std::endl; 1090 | std::cout << " Preserve Leading Spaces: " << m_preserveLeadingSpaces << std::endl; 1091 | std::cout << " Preserve Trailing Spaces: " << m_preserveTrailingSpaces << std::endl; 1092 | std::cout << " Allow Word Breaks: " << m_allowWordBreaks << std::endl; 1093 | std::cout << " Demote Newlines: " << m_demoteNewlines << std::endl; 1094 | std::cout << " Justify Text: " << m_justifyText << std::endl; 1095 | std::cout << std::endl; 1096 | std::cout << "========================================================" << std::endl; 1097 | std::cout << "LINE COUNT: " << lines.size() << std::endl; 1098 | std::cout << "========================================================" << std::endl; 1099 | 1100 | for (auto s = 0u; s < lines.size(); ++s) { 1101 | 1102 | auto const & line = lines.at(s); 1103 | 1104 | std::cout 1105 | << std::endl 1106 | << "Line #" << s << std::endl 1107 | << " inputOffset = " << line.inputOffset << std::endl 1108 | << " inputLength = " << line.inputLength << std::endl 1109 | << " outputOffset = " << line.outputOffset << std::endl 1110 | << " outputLength = " << line.outputLength << std::endl 1111 | ; 1112 | 1113 | for (auto t = 0u; t < line.tokens.size(); ++t) { 1114 | 1115 | auto const & token = line.tokens.at(t); 1116 | 1117 | std::cout 1118 | << std::endl 1119 | << " Token #" << t << std::endl 1120 | << " inputOffset = " << token.inputOffset << std::endl 1121 | << " inputLength = " << token.inputLength << std::endl 1122 | << " outputOffset = " << token.outputOffset << std::endl 1123 | << " outputLength = " << token.outputLength << std::endl 1124 | << " string = '" << token.string.toString() << "'" << std::endl 1125 | ; 1126 | 1127 | } 1128 | 1129 | } 1130 | } 1131 | 1132 | void TextLayout::dump(void) const 1133 | { 1134 | this->dump(m_lines); 1135 | } 1136 | 1137 | #endif 1138 | -------------------------------------------------------------------------------- /sources/TextLayout.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "./LineSizeContainer.hh" 7 | #include "./Line.hh" 8 | #include "./Position.hh" 9 | #include "./StringContainer.hh" 10 | #include "./TextOperation.hh" 11 | #include "./TokenLocator.hh" 12 | 13 | class TextLayout { 14 | 15 | public: 16 | 17 | TextLayout(void); 18 | 19 | public: // options getters 20 | 21 | unsigned getColumns(void) const; 22 | unsigned getTabWidth(void) const; 23 | 24 | bool getSoftWrap(void) const; 25 | bool getCollapseWhitespaces(void) const; 26 | bool getPreserveLeadingSpaces(void) const; 27 | bool getPreserveTrailingSpaces(void) const; 28 | bool getAllowWordBreaks(void) const; 29 | bool getDemoteNewlines(void) const; 30 | bool getJustifyText(void) const; 31 | 32 | public: // options setters 33 | 34 | bool setColumns(unsigned columns); 35 | bool setTabWidth(unsigned tabWidth); 36 | 37 | bool setSoftWrap(bool softWrap); 38 | bool setCollapseWhitespaces(bool collapseWhitespaces); 39 | bool setPreserveLeadingSpaces(bool preserveLeadingSpaces); 40 | bool setPreserveTrailingSpaces(bool preserveTrailingSpaces); 41 | bool setAllowWordBreaks(bool allowWordBreaks); 42 | bool setDemoteNewlines(bool demoteNewlines); 43 | bool setJustifyText(bool justifyText); 44 | 45 | public: // state info getters 46 | 47 | unsigned getRowCount(void) const; 48 | unsigned getColumnCount(void) const; 49 | unsigned getSoftWrapCount(void) const; 50 | unsigned getMaxCharacterIndex(void) const; 51 | 52 | Position getFirstPosition(void) const; 53 | Position getLastPosition(void) const; 54 | 55 | bool doesSoftWrap(unsigned row) const; 56 | 57 | std::string getSource(void) const; 58 | std::string getText(void) const; 59 | std::string getLine(unsigned row) const; 60 | unsigned getLineLength(unsigned row) const; 61 | std::string getLineSlice(unsigned row, unsigned start, unsigned end) const; 62 | 63 | public: // cursor management 64 | 65 | Position getFixedCellPosition(Position position) const; 66 | Position getFixedPosition(Position position) const; 67 | 68 | std::pair getPositionLeft(Position position) const; 69 | std::pair getPositionRight(Position position) const; 70 | 71 | std::pair getPositionAbove(Position position) const; 72 | std::pair getPositionAbove(Position position, unsigned amplitude) const; 73 | 74 | std::pair getPositionBelow(Position position) const; 75 | std::pair getPositionBelow(Position position, unsigned amplitude) const; 76 | 77 | public: // pointers conversions 78 | 79 | unsigned getRowForCharacterIndex(unsigned characterIndex) const; 80 | unsigned getCharacterIndexForRow(unsigned row) const; 81 | 82 | Position getPositionForCharacterIndex(unsigned characterIndex) const; 83 | unsigned getCharacterIndexForPosition(Position position) const; 84 | 85 | public: // state mutators 86 | 87 | TextOperation applyConfiguration(void); 88 | 89 | TextOperation clearSource(void); 90 | TextOperation setSource(std::string const & source); 91 | TextOperation spliceSource(unsigned start, unsigned deleted, std::string const & source); 92 | 93 | #ifdef DEBUG 94 | 95 | public: // debug only 96 | 97 | void dump(std::vector const & generatedLines) const; 98 | void dump(void) const; 99 | 100 | #endif 101 | 102 | private: 103 | 104 | TextOperation update(unsigned start, unsigned deleted, unsigned added); 105 | void apply(TextOperation const & textOperation); 106 | 107 | private: 108 | 109 | TokenLocator findTokenLocatorForPosition(Position const & position) const; 110 | 111 | private: 112 | 113 | unsigned m_columns; 114 | unsigned m_tabWidth; 115 | 116 | bool m_softWrap; 117 | bool m_collapseWhitespaces; 118 | bool m_preserveLeadingSpaces; 119 | bool m_preserveTrailingSpaces; 120 | bool m_allowWordBreaks; 121 | bool m_demoteNewlines; 122 | bool m_justifyText; 123 | 124 | StringContainer m_source; 125 | 126 | LineSizeContainer m_lineSizeContainer; 127 | unsigned m_softWrapCount; 128 | 129 | std::vector m_lines; 130 | 131 | }; 132 | -------------------------------------------------------------------------------- /sources/TextOperation.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "./Line.hh" 7 | 8 | struct TextOperation { 9 | 10 | // The index of the line from which we need to start update 11 | unsigned startingRow; 12 | 13 | // The number of lines that have to be removed 14 | unsigned deletedLineCount; 15 | 16 | // The number of lines that have been added 17 | unsigned addedLineCount; 18 | 19 | // The vector of generated lines 20 | std::vector addedLines; 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /sources/Token.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "./StringContainer.hh" 6 | 7 | enum TokenType { 8 | 9 | TOKEN_DYNAMIC, 10 | TOKEN_WHITESPACES, 11 | TOKEN_WORD 12 | 13 | }; 14 | 15 | struct Token { 16 | 17 | TokenType type; 18 | 19 | unsigned inputOffset; 20 | unsigned inputLength; 21 | 22 | unsigned outputOffset; 23 | unsigned outputLength; 24 | 25 | bool canBeSubdivided; 26 | 27 | StringContainer string; 28 | 29 | Token(void) 30 | : type(TOKEN_DYNAMIC) 31 | , inputOffset(0) 32 | , inputLength(0) 33 | , outputOffset(0) 34 | , outputLength(0) 35 | , canBeSubdivided(false) 36 | { 37 | } 38 | 39 | Token(TokenType type) 40 | : type(type) 41 | , inputOffset(0) 42 | , inputLength(0) 43 | , outputOffset(0) 44 | , outputLength(0) 45 | , canBeSubdivided(false) 46 | { 47 | } 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /sources/TokenLocator.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./Line.hh" 4 | #include "./Token.hh" 5 | 6 | struct TokenLocator { 7 | 8 | unsigned row; 9 | unsigned tokenIndex; 10 | 11 | Line const & line; 12 | Token const & token; 13 | 14 | TokenLocator(unsigned row, unsigned tokenIndex, Line const & line, Token const & token) 15 | : row(row) 16 | , tokenIndex(tokenIndex) 17 | , line(line) 18 | , token(token) 19 | { 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /sources/embind.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "./Position.hh" 4 | #include "./TextLayout.hh" 5 | #include "./TextOperation.hh" 6 | 7 | EMSCRIPTEN_BINDINGS(text_layout) 8 | { 9 | using namespace emscripten; 10 | 11 | register_vector("std::vector"); 12 | 13 | value_array>("std::pair") 14 | .element(&std::pair::first) 15 | .element(&std::pair::second); 16 | 17 | value_object("TextOperation") 18 | .field("startingRow", &TextOperation::startingRow) 19 | .field("deletedLineCount", &TextOperation::deletedLineCount) 20 | .field("addedLineCount", &TextOperation::addedLineCount); 21 | 22 | value_object("Position") 23 | .field("x", &Position::x) 24 | .field("y", &Position::y); 25 | 26 | class_("TextLayout") 27 | .constructor<>() 28 | 29 | .function("getColumns", &TextLayout::getColumns) 30 | .function("getTabWidth", &TextLayout::getTabWidth) 31 | .function("getSoftWrap", &TextLayout::getSoftWrap) 32 | .function("getCollapseWhitespaces", &TextLayout::getCollapseWhitespaces) 33 | .function("getPreserveLeadingSpaces", &TextLayout::getPreserveLeadingSpaces) 34 | .function("getPreserveTrailingSpaces", &TextLayout::getPreserveTrailingSpaces) 35 | .function("getAllowWordBreaks", &TextLayout::getAllowWordBreaks) 36 | .function("getDemoteNewlines", &TextLayout::getDemoteNewlines) 37 | .function("getJustifyText", &TextLayout::getJustifyText) 38 | 39 | .function("setColumns", &TextLayout::setColumns) 40 | .function("setTabWidth", &TextLayout::setTabWidth) 41 | .function("setSoftWrap", &TextLayout::setSoftWrap) 42 | .function("setCollapseWhitespaces", &TextLayout::setCollapseWhitespaces) 43 | .function("setPreserveLeadingSpaces", &TextLayout::setPreserveLeadingSpaces) 44 | .function("setPreserveTrailingSpaces", &TextLayout::setPreserveTrailingSpaces) 45 | .function("setAllowWordBreaks", &TextLayout::setAllowWordBreaks) 46 | .function("setDemoteNewlines", &TextLayout::setDemoteNewlines) 47 | .function("setJustifyText", &TextLayout::setJustifyText) 48 | 49 | .function("getRowCount", &TextLayout::getRowCount) 50 | .function("getColumnCount", &TextLayout::getColumnCount) 51 | .function("getSoftWrapCount", &TextLayout::getSoftWrapCount) 52 | .function("getMaxCharacterIndex", &TextLayout::getMaxCharacterIndex) 53 | .function("getFirstPosition", &TextLayout::getFirstPosition) 54 | .function("getLastPosition", &TextLayout::getLastPosition) 55 | .function("doesSoftWrap", &TextLayout::doesSoftWrap) 56 | .function("getSource", &TextLayout::getSource) 57 | .function("getText", &TextLayout::getText) 58 | .function("getLine", &TextLayout::getLine) 59 | .function("getLineLength", &TextLayout::getLineLength) 60 | .function("getLineSlice", &TextLayout::getLineSlice) 61 | 62 | .function("getFixedCellPosition", &TextLayout::getFixedCellPosition) 63 | .function("getFixedPosition", &TextLayout::getFixedPosition) 64 | .function("getPositionLeft", &TextLayout::getPositionLeft) 65 | .function("getPositionRight", &TextLayout::getPositionRight) 66 | .function("getPositionAbove", select_overload (Position) const>(&TextLayout::getPositionAbove)) 67 | .function("getPositionAbove", select_overload (Position, unsigned) const>(&TextLayout::getPositionAbove)) 68 | .function("getPositionBelow", select_overload (Position) const>(&TextLayout::getPositionBelow)) 69 | .function("getPositionBelow", select_overload (Position, unsigned) const>(&TextLayout::getPositionBelow)) 70 | 71 | .function("getRowForCharacterIndex", &TextLayout::getRowForCharacterIndex) 72 | .function("getCharacterIndexForRow", &TextLayout::getCharacterIndexForRow) 73 | 74 | .function("getPositionForCharacterIndex", &TextLayout::getPositionForCharacterIndex) 75 | .function("getCharacterIndexForPosition", &TextLayout::getCharacterIndexForPosition) 76 | 77 | .function("applyConfiguration", &TextLayout::applyConfiguration) 78 | 79 | .function("clearSource", &TextLayout::clearSource) 80 | .function("setSource", &TextLayout::setSource) 81 | .function("spliceSource", &TextLayout::spliceSource) 82 | 83 | #ifdef DEBUG 84 | .function("dump", select_overload(&TextLayout::dump)) 85 | #endif 86 | ; 87 | } 88 | -------------------------------------------------------------------------------- /sources/run-tests.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`); 2 | const ts = require(`term-strings`); 3 | const glob = require(`glob`); 4 | const vm = require(`vm`); 5 | 6 | const wasm = fs.readFileSync(require.resolve(`mono-layout/wasm`)); 7 | 8 | const {createContext} = require(`mono-layout/sync`); 9 | const {createLayout} = createContext(wasm); 10 | 11 | const ok = `${ts.style.color.front(`green`)}✓${ts.style.color.front.out}`; 12 | const ko = `${ts.style.color.front(`red`)}✗${ts.style.color.front.out}`; 13 | 14 | class TestSuite { 15 | 16 | constructor() { 17 | 18 | this.tests = []; 19 | 20 | } 21 | 22 | register(label) { 23 | 24 | let test = { label, fn: () => () => {} }; 25 | this.tests.push(test); 26 | 27 | return test; 28 | 29 | } 30 | 31 | run(level = 0) { 32 | 33 | let indent = ` `.repeat(level * 4); 34 | 35 | if (level > 0 && this.tests.length > 0) 36 | console.log(``); 37 | 38 | for (let test of this.tests) { 39 | 40 | let testsuite = new TestSuite(); 41 | 42 | try { 43 | test.fn(testsuite, makeEnv()); 44 | console.log(`${indent} ${ok} ${test.label}`); 45 | } catch (err) { 46 | console.log(`${indent} ${ko} ${test.label} (${err.message || err})`); 47 | } 48 | 49 | testsuite.run(level + 1); 50 | 51 | } 52 | 53 | if (level > 0 && this.tests.length > 0) { 54 | console.log(``); 55 | } 56 | 57 | } 58 | 59 | } 60 | 61 | function makeEnv() { 62 | 63 | let layout = createLayout(); 64 | 65 | let otp = [ '' ]; 66 | 67 | function APPLY(patch) { 68 | 69 | otp.splice(patch.startingRow, patch.deletedLineCount, ...Array.from({length: patch.addedLineCount}, (_, n) => layout.getLine(patch.startingRow + n))); 70 | 71 | } 72 | 73 | function SETUP_EMPTY() { 74 | 75 | } 76 | 77 | function SETUP(newStr) { 78 | 79 | str = newStr; 80 | 81 | APPLY(layout.setSource(newStr)); 82 | 83 | } 84 | 85 | function RESET() { 86 | 87 | APPLY(layout.applyConfiguration()); 88 | 89 | } 90 | 91 | function SET_SOURCE(newStr) { 92 | 93 | APPLY(layout.setSource(newStr)); 94 | 95 | } 96 | 97 | function SPLICE(start, length, replacement) { 98 | 99 | APPLY(layout.spliceSource(start, length, replacement)); 100 | 101 | } 102 | 103 | function APPEND(appendStr) { 104 | 105 | SPLICE(str.length, 0, appendStr); 106 | 107 | } 108 | 109 | function LINE_COUNT() { 110 | 111 | return otp.length; 112 | 113 | } 114 | 115 | function TEXT() { 116 | 117 | return otp.join(`\n`); 118 | 119 | } 120 | 121 | function LINE_SLICE(row, start, end) { 122 | 123 | return layout.getLineSlice(row, start, end); 124 | 125 | } 126 | 127 | function REQUIRE(condition, msg = `Assertion failed!`) { 128 | 129 | if (!condition) { 130 | throw new Error(msg); 131 | } 132 | 133 | } 134 | 135 | function ASSERT_EQ(left, right) { 136 | 137 | REQUIRE(JSON.stringify(left) === JSON.stringify(right), `${JSON.stringify(left)} == ${JSON.stringify(right)}`); 138 | 139 | } 140 | 141 | return { layout, SETUP_EMPTY, SETUP, RESET, SET_SOURCE, SPLICE, APPEND, LINE_COUNT, LINE_SLICE, TEXT, REQUIRE, ASSERT_EQ, Position: (x, y) => ({x, y}), PositionRet: (x, y, perfectFit) => [{x, y}, perfectFit] }; 142 | 143 | } 144 | 145 | let testsuite = new TestSuite(); 146 | 147 | for (let file of glob.sync(`**/*.test.cc`, { cwd: __dirname })) { 148 | 149 | let content = fs.readFileSync(`${__dirname}/${file}`).toString(); 150 | 151 | if (!content.includes(`ASSERT`)) 152 | continue; 153 | 154 | content = content.replace(/^[ \t]*#.*/gm, ``); 155 | content = content.replace(/(TEST_CASE|SECTION)\((.*)\)$/gm, `testsuite.register($2).fn = (testsuite, env) =>`); 156 | content = content.replace(/FOR\(([^,]+),/g, `for (let $1 of `); 157 | content = content.replace(/([A-Z][A-Z_]*)\(/g, `env.$1(`); 158 | content = content.replace(/\b(layout|Position|PositionRet)\b/g, `env.$1`); 159 | content = content.replace(/([0-9])u/g, `$1`); 160 | content = content.replace(/\bauto\b/g, `let`); 161 | 162 | testsuite.register(file).fn = testsuite => { 163 | vm.runInNewContext(content, { console, JSON, testsuite }); 164 | }; 165 | 166 | } 167 | 168 | testsuite.run(); 169 | -------------------------------------------------------------------------------- /sources/tests/framework.hh: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "./../tools/TextOutput.hh" 8 | #include "./../TextLayout.hh" 9 | 10 | #define SETUP_EMPTY() \ 11 | \ 12 | TextLayout layout = TextLayout(); \ 13 | 14 | #define SETUP(STR) \ 15 | \ 16 | TextLayout layout = TextLayout(); \ 17 | \ 18 | TextOutput output = TextOutput(); \ 19 | output.apply(layout.setSource(STR)); \ 20 | 21 | #define SPLICE(OFFSET, LENGTH, REPLACEMENT) \ 22 | { \ 23 | auto offset = (OFFSET); \ 24 | auto length = (LENGTH); \ 25 | \ 26 | std::ostringstream replacementBuilder; \ 27 | replacementBuilder << (REPLACEMENT); \ 28 | std::string replacement = replacementBuilder.str(); \ 29 | \ 30 | output.apply(layout.spliceSource(offset, length, replacement)); \ 31 | } \ 32 | 33 | #define APPEND(STRING) \ 34 | { \ 35 | SPLICE(layout.getMaxCharacterIndex(), 0, STRING); \ 36 | } \ 37 | 38 | #define RESET() \ 39 | { \ 40 | output.apply(layout.applyConfiguration()); \ 41 | } \ 42 | 43 | #define SET_SOURCE(SOURCE) \ 44 | ({ \ 45 | output.apply(layout.setSource(SOURCE)); \ 46 | }) \ 47 | 48 | #define LINE_SLICE(ROW, START, END) \ 49 | ({ \ 50 | layout.getLineSlice(ROW, START, END); \ 51 | }) \ 52 | 53 | #define TEXT() \ 54 | ({ \ 55 | output.getText(); \ 56 | }) \ 57 | 58 | #define LINE_COUNT() \ 59 | ({ \ 60 | output.getLineCount(); \ 61 | }) \ 62 | 63 | #define FOR(C, STR) \ 64 | \ 65 | for (auto c : std::string(STR)) 66 | 67 | #define ASSERT_EQ(LEFT, RIGHT) \ 68 | \ 69 | REQUIRE(LEFT == RIGHT) 70 | 71 | #define PositionRet(X, Y, PERFECT_FIT) std::pair(Position(X, Y), PERFECT_FIT) 72 | -------------------------------------------------------------------------------- /sources/tests/general.test.cc: -------------------------------------------------------------------------------- 1 | #include "./framework.hh" 2 | 3 | TEST_CASE("it should have a single empty line when parsing an empty string") 4 | { 5 | SETUP(""); 6 | 7 | ASSERT_EQ(LINE_COUNT(), 1); 8 | ASSERT_EQ(TEXT(), ""); 9 | } 10 | 11 | TEST_CASE("it should correctly parse a single line") 12 | { 13 | SETUP("Hello World"); 14 | 15 | ASSERT_EQ(LINE_COUNT(), 1); 16 | ASSERT_EQ(TEXT(), "Hello World"); 17 | } 18 | 19 | TEST_CASE("it should correctly parse multiple lines") 20 | { 21 | SETUP("Hello\nWorld"); 22 | 23 | ASSERT_EQ(LINE_COUNT(), 2); 24 | ASSERT_EQ(TEXT(), "Hello\nWorld"); 25 | } 26 | 27 | TEST_CASE("it should allow replacing a text by another") 28 | { 29 | SETUP("Hello World"); 30 | SET_SOURCE("Something different"); 31 | 32 | ASSERT_EQ(LINE_COUNT(), 1); 33 | ASSERT_EQ(TEXT(), "Something different"); 34 | } 35 | 36 | TEST_CASE("it should support ending a text with a newline character") 37 | { 38 | SETUP("Hello World\n"); 39 | 40 | ASSERT_EQ(LINE_COUNT(), 2); 41 | ASSERT_EQ(TEXT(), "Hello World\n"); 42 | } 43 | 44 | TEST_CASE("it should correctly normalize characters") 45 | { 46 | SETUP("Hello\tWorld\nThis is a\rtest."); 47 | 48 | ASSERT_EQ(LINE_COUNT(), 3); 49 | ASSERT_EQ(TEXT(), "Hello World\nThis is a\ntest."); 50 | } 51 | 52 | TEST_CASE("it should correctly wrap text") 53 | { 54 | SETUP("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 55 | 56 | layout.setColumns(4); 57 | layout.setSoftWrap(true); 58 | RESET(); 59 | 60 | ASSERT_EQ(LINE_COUNT(), 7); 61 | ASSERT_EQ(TEXT(), "ABCD\nEFGH\nIJKL\nMNOP\nQRST\nUVWX\nYZ"); 62 | } 63 | 64 | TEST_CASE("it should support zero-size column mode") 65 | { 66 | SETUP("Hello world"); 67 | 68 | layout.setColumns(0); 69 | layout.setSoftWrap(true); 70 | RESET(); 71 | 72 | ASSERT_EQ(TEXT(), ""); 73 | } 74 | 75 | TEST_CASE("it should support leaving zero-size column mode") 76 | { 77 | SETUP("Hello world"); 78 | 79 | layout.setColumns(0); 80 | layout.setSoftWrap(true); 81 | RESET(); 82 | 83 | layout.setSoftWrap(false); 84 | RESET(); 85 | 86 | ASSERT_EQ(TEXT(), "Hello world"); 87 | } 88 | 89 | TEST_CASE("it should avoid breaking words unless allowed to") 90 | { 91 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 92 | 93 | layout.setColumns(8); 94 | layout.setSoftWrap(true); 95 | RESET(); 96 | 97 | ASSERT_EQ(LINE_COUNT(), 8); 98 | ASSERT_EQ(TEXT(), "Horse\nTiger\nSnake\nZebra\nMouse\nSheep\nWhale\nPanda"); 99 | 100 | layout.setAllowWordBreaks(true); 101 | RESET(); 102 | 103 | ASSERT_EQ(LINE_COUNT(), 6); 104 | ASSERT_EQ(TEXT(), "Horse Ti\nger Snak\ne Zebra\nMouse Sh\neep Whal\ne Panda"); 105 | } 106 | 107 | TEST_CASE("it should collapse whitespaces if requested") 108 | { 109 | SETUP("Hello world \t test!"); 110 | 111 | layout.setCollapseWhitespaces(true); 112 | RESET(); 113 | 114 | ASSERT_EQ(TEXT(), "Hello world test!"); 115 | } 116 | 117 | TEST_CASE("it should justify the text if requested") 118 | { 119 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 120 | 121 | layout.setColumns(14); 122 | layout.setSoftWrap(true); 123 | layout.setCollapseWhitespaces(true); 124 | layout.setJustifyText(true); 125 | RESET(); 126 | 127 | ASSERT_EQ(TEXT(), "Horse Tiger\nSnake Zebra\nMouse Sheep\nWhale Panda"); 128 | } 129 | 130 | TEST_CASE("it should support updating a single line") 131 | { 132 | SETUP("Hello World\nThis is a test"); 133 | SPLICE(6, 5, "Toto"); 134 | 135 | ASSERT_EQ(TEXT(), "Hello Toto\nThis is a test"); 136 | } 137 | 138 | TEST_CASE("it should support updating multiple lines") 139 | { 140 | SETUP("Horse\nTiger\nSnake\nZebra\nMouse\nSheep\nWhale\nPanda"); 141 | SPLICE(7, 9, "atou\nSwin"); 142 | 143 | ASSERT_EQ(TEXT(), "Horse\nTatou\nSwine\nZebra\nMouse\nSheep\nWhale\nPanda"); 144 | } 145 | 146 | TEST_CASE("it should support adding new lines when updating") 147 | { 148 | SETUP("Horse\nTiger\nSnake\nZebra\nMouse\nSheep\nWhale\nPanda"); 149 | SPLICE(7, 9, "iger\nTatoo\nSwine\nSnak"); 150 | 151 | ASSERT_EQ(TEXT(), "Horse\nTiger\nTatoo\nSwine\nSnake\nZebra\nMouse\nSheep\nWhale\nPanda"); 152 | } 153 | 154 | TEST_CASE("it should support removing lines when updating") 155 | { 156 | SETUP("Horse\nTiger\nSnake\nZebra\nMouse\nSheep\nWhale\nPanda"); 157 | SPLICE(13, 18, ""); 158 | 159 | ASSERT_EQ(TEXT(), "Horse\nTiger\nSheep\nWhale\nPanda"); 160 | } 161 | 162 | TEST_CASE("it should support removing the last newline character") 163 | { 164 | SETUP("Hello World\n"); 165 | SPLICE(11, 1, ""); 166 | 167 | ASSERT_EQ(TEXT(), "Hello World"); 168 | } 169 | 170 | TEST_CASE("it should support removing a newline character among many") 171 | { 172 | SETUP("Hello World\n\n\n"); 173 | SPLICE(12, 1, ""); 174 | 175 | ASSERT_EQ(TEXT(), "Hello World\n\n"); 176 | } 177 | 178 | TEST_CASE("it should not delete the last newline when removing the only character that immediatly follows it") 179 | { 180 | SETUP("Hello World\n!"); 181 | SPLICE(12, 1, ""); 182 | 183 | ASSERT_EQ(TEXT(), "Hello World\n"); 184 | } 185 | -------------------------------------------------------------------------------- /sources/tests/main.test.cc: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | -------------------------------------------------------------------------------- /sources/tests/methods.test.cc: -------------------------------------------------------------------------------- 1 | #include "./framework.hh" 2 | 3 | TEST_CASE("#positionForCharacterIndex()") 4 | { 5 | SECTION("it should return the right position for a given character index inside the original text") 6 | { 7 | SETUP("Hello World\nFoo Bar!"); 8 | 9 | ASSERT_EQ(layout.getPositionForCharacterIndex(4), Position(4, 0)); 10 | ASSERT_EQ(layout.getPositionForCharacterIndex(15), Position(3, 1)); 11 | } 12 | 13 | SECTION("it should work even when the first characters are skipped") 14 | { 15 | SETUP(" Hello World!"); 16 | 17 | ASSERT_EQ(layout.getPositionForCharacterIndex(5), Position(1, 0)); 18 | } 19 | 20 | SECTION("it should work even for the last character") 21 | { 22 | SETUP("Hello World"); 23 | 24 | ASSERT_EQ(layout.getPositionForCharacterIndex(11), Position(11, 0)); 25 | } 26 | } 27 | 28 | TEST_CASE("#getPositionLeft()") 29 | { 30 | SECTION("it should be able to move inside subdividable tokens") 31 | { 32 | SETUP("Hello"); 33 | 34 | ASSERT_EQ(layout.getPositionLeft(Position(3, 0)), PositionRet(2, 0, true)); 35 | } 36 | 37 | SECTION("it should jump over tokens that can't be subdivided") 38 | { 39 | SETUP("Hello\tWorld"); 40 | 41 | ASSERT_EQ(layout.getPositionLeft(Position(9, 0)), PositionRet(5, 0, true)); 42 | } 43 | 44 | SECTION("should jump to the end of the previous line when already on the left edge of a line") 45 | { 46 | SETUP("Hello World\nThis is a test"); 47 | 48 | ASSERT_EQ(layout.getPositionLeft(Position(0, 1)), PositionRet(11, 0, true)); 49 | } 50 | 51 | SECTION("shouldn't do anything when already on the topmost position") 52 | { 53 | SETUP("Hello World"); 54 | 55 | ASSERT_EQ(layout.getPositionLeft(Position(0, 0)), PositionRet(0, 0, true)); 56 | } 57 | } 58 | 59 | TEST_CASE("#getPositionRight()") 60 | { 61 | SECTION("it should be able to move inside subdividable tokens") 62 | { 63 | SETUP("Hello"); 64 | 65 | ASSERT_EQ(layout.getPositionRight(Position(2, 0)), PositionRet(3, 0, true)); 66 | } 67 | 68 | SECTION("it should jump over tokens that can't be subdivided") 69 | { 70 | SETUP("Hello\tWorld"); 71 | 72 | ASSERT_EQ(layout.getPositionRight(Position(5, 0)), PositionRet(9, 0, true)); 73 | } 74 | 75 | SECTION("should jump to the beginning of the next line when already on the right edge of a line") 76 | { 77 | SETUP("Hello World\nThis is a test"); 78 | 79 | ASSERT_EQ(layout.getPositionRight(Position(11, 0)), PositionRet(0, 1, true)); 80 | } 81 | 82 | SECTION("shouldn't do anything when already on the lowest position") 83 | { 84 | SETUP("Hello World"); 85 | 86 | ASSERT_EQ(layout.getPositionRight(Position(11, 0)), PositionRet(11, 0, true)); 87 | } 88 | } 89 | 90 | TEST_CASE("#getPositionAbove()") 91 | { 92 | SECTION("it should be able to move a cursor inside subdividable tokens") 93 | { 94 | SETUP("Hello\nWorld"); 95 | 96 | ASSERT_EQ(layout.getPositionAbove(Position(2, 1), 1), PositionRet(2, 0, true)); 97 | } 98 | 99 | SECTION("it should prevent jumping inside tokens that cannot be subdivided") 100 | { 101 | SETUP("Hello\tWorld\nThis is a test"); 102 | 103 | ASSERT_EQ(layout.getPositionAbove(Position(6, 1), 1), PositionRet(5, 0, true)); 104 | ASSERT_EQ(layout.getPositionAbove(Position(8, 1), 1), PositionRet(9, 0, true)); 105 | } 106 | 107 | SECTION("it should move to the beginning of the line when already on the very first line") 108 | { 109 | SETUP("Hello World"); 110 | 111 | ASSERT_EQ(layout.getPositionAbove(Position(5, 0), 1), PositionRet(0, 0, true)); 112 | } 113 | 114 | SECTION("it should move to the end of the line when we would jump outside") 115 | { 116 | SETUP("Hello\nThis is a test\nWorld"); 117 | 118 | ASSERT_EQ(layout.getPositionAbove(Position(10, 1)), PositionRet(5, 0, false)); 119 | } 120 | } 121 | 122 | TEST_CASE("#getPositionBelow()") 123 | { 124 | SECTION("it should be able to move a cursor inside subdividable tokens") 125 | { 126 | SETUP("Hello\nWorld"); 127 | 128 | ASSERT_EQ(layout.getPositionBelow(Position(2, 0), 1), PositionRet(2, 1, true)); 129 | } 130 | 131 | SECTION("it should prevent jumping inside tokens that cannot be subdivided") 132 | { 133 | SETUP("This is a test\nHello\tWorld"); 134 | 135 | ASSERT_EQ(layout.getPositionBelow(Position(6, 0), 1), PositionRet(5, 1, true)); 136 | ASSERT_EQ(layout.getPositionBelow(Position(8, 0), 1), PositionRet(9, 1, true)); 137 | } 138 | 139 | SECTION("it should move to the end of the line when already on the very last line") 140 | { 141 | SETUP("Hello World"); 142 | 143 | ASSERT_EQ(layout.getPositionBelow(Position(5, 0), 1), PositionRet(11, 0, true)); 144 | } 145 | 146 | SECTION("it should move to the end of the line when we would jump outside") 147 | { 148 | SETUP("Hello\nThis is a test\nWorld"); 149 | 150 | ASSERT_EQ(layout.getPositionBelow(Position(10, 1)), PositionRet(5, 2, false)); 151 | } 152 | } 153 | 154 | TEST_CASE("#getRowCount()") 155 | { 156 | SECTION("it should return the right number of rows in the document") 157 | { 158 | SETUP("Hello World\nThis is a test\nFoo Bar\n"); 159 | 160 | ASSERT_EQ(layout.getRowCount(), 4); 161 | } 162 | 163 | SECTION("it should also count soft-wrapped lines") 164 | { 165 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 166 | 167 | layout.setColumns(8); 168 | layout.setSoftWrap(true); 169 | RESET(); 170 | 171 | ASSERT_EQ(layout.getRowCount(), 8); 172 | } 173 | } 174 | 175 | TEST_CASE("#getColumnCount()") 176 | { 177 | SECTION("it should return 0 on an uninitialized buffer") 178 | { 179 | SETUP_EMPTY(); 180 | 181 | ASSERT_EQ(layout.getColumnCount(), 0); 182 | } 183 | 184 | SECTION("it should return the maximal number of columns in the document") 185 | { 186 | SETUP("This is a test\nHello World\nSanctus Dominus Infernus\n"); 187 | 188 | ASSERT_EQ(layout.getColumnCount(), 24); 189 | } 190 | 191 | SECTION("it should be correctly set when the layout has a maximal number of column lower than needed") 192 | { 193 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 194 | 195 | layout.setColumns(8); 196 | layout.setSoftWrap(true); 197 | RESET(); 198 | 199 | ASSERT_EQ(layout.getColumnCount(), 5); 200 | } 201 | 202 | SECTION("it should be correctly updated when the layout options are updated") 203 | { 204 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 205 | 206 | layout.setColumns(8); 207 | layout.setSoftWrap(true); 208 | RESET(); 209 | 210 | ASSERT_EQ(layout.getColumnCount(), 5); 211 | 212 | layout.setAllowWordBreaks(true); 213 | RESET(); 214 | 215 | ASSERT_EQ(layout.getColumnCount(), 8); 216 | } 217 | 218 | SECTION("it should be correctly updated when lines are removed") 219 | { 220 | SETUP("This is a test\nHello World\nSanctus Dominus Infernus\n"); 221 | 222 | SPLICE(27, 24, ""); 223 | 224 | ASSERT_EQ(layout.getColumnCount(), 14); 225 | } 226 | 227 | SECTION("it should be correctly updated when lines are added") 228 | { 229 | SETUP("This is a test\nHello World\nSanctus Dominus Infernus\n"); 230 | 231 | SPLICE(27, 0, "Space 1992: Rise of the Chaos Wizards\n"); 232 | 233 | ASSERT_EQ(layout.getColumnCount(), 37); 234 | } 235 | 236 | SECTION("it should be correctly updated when lines are expanded") 237 | { 238 | SETUP("This is a test\nHello World\nSanctus Dominus Infernus\n"); 239 | 240 | SPLICE(15, 0, "As Foobar Sayed: "); 241 | 242 | ASSERT_EQ(layout.getColumnCount(), 28); 243 | } 244 | 245 | SECTION("it should be correctly updated when lines are shrinked") 246 | { 247 | SETUP("This is a test\nHello World\nSanctus Dominus Infernus\n"); 248 | 249 | SPLICE(27, 16, ""); 250 | 251 | ASSERT_EQ(layout.getColumnCount(), 14); 252 | } 253 | } 254 | 255 | TEST_CASE("#getSoftWrapCount()") 256 | { 257 | SECTION("it should return the number of soft-wrap-generated lines accross the text") 258 | { 259 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 260 | 261 | layout.setColumns(8); 262 | layout.setSoftWrap(true); 263 | RESET(); 264 | 265 | ASSERT_EQ(layout.getSoftWrapCount(), 7); 266 | } 267 | 268 | SECTION("it shouldn't count the last line as being soft-wrapped, even when being as large as the maximal size") 269 | { 270 | SETUP("Horse Tiger Snake Zebra Mouse Sheep Whale Panda"); 271 | 272 | layout.setColumns(5); 273 | layout.setSoftWrap(true); 274 | RESET(); 275 | 276 | ASSERT_EQ(layout.getSoftWrapCount(), 7); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /sources/tests/stresstests.test.cc: -------------------------------------------------------------------------------- 1 | #include "./framework.hh" 2 | 3 | TEST_CASE("stress test #1") 4 | { 5 | SETUP(""); 6 | 7 | for (auto t = 0u; t < 30; ++t) 8 | APPEND("Foo\n"); 9 | 10 | ASSERT_EQ(LINE_COUNT(), 31); 11 | ASSERT_EQ(TEXT(), "Foo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\nFoo\n"); 12 | } 13 | 14 | TEST_CASE("stress test #2") 15 | { 16 | SETUP(""); 17 | 18 | for (auto t = 0u; t < 30; ++t) { 19 | APPEND("a"); 20 | APPEND(" "); 21 | } 22 | 23 | ASSERT_EQ(LINE_COUNT(), 1); 24 | ASSERT_EQ(TEXT(), "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a"); 25 | } 26 | 27 | TEST_CASE("stress test #3") 28 | { 29 | SETUP(""); 30 | 31 | layout.setColumns(10); 32 | layout.setSoftWrap(true); 33 | layout.setCollapseWhitespaces(true); 34 | layout.setJustifyText(true); 35 | RESET(); 36 | 37 | for (auto t = 0u; t < 30; ++t) { 38 | APPEND("a"); 39 | APPEND(" "); 40 | } 41 | 42 | ASSERT_EQ(LINE_COUNT(), 6); 43 | ASSERT_EQ(TEXT(), "a a a a a\na a a a a\na a a a a\na a a a a\na a a a a\na a a a a"); 44 | } 45 | 46 | TEST_CASE("stress test #4") 47 | { 48 | SETUP(""); 49 | 50 | layout.setColumns(10); 51 | layout.setSoftWrap(true); 52 | layout.setCollapseWhitespaces(true); 53 | layout.setPreserveTrailingSpaces(true); 54 | layout.setJustifyText(true); 55 | RESET(); 56 | 57 | auto currentPosition = Position(0, 0); 58 | 59 | FOR(c, "a b c d e f g h i j k l m n o p q r s t u v w x y z\nA B C D E F G H I J K L M N O P Q R S T U V W X Y Z") { 60 | auto characterIndex = layout.getCharacterIndexForPosition(currentPosition); 61 | SPLICE(characterIndex, 0, c); 62 | currentPosition = layout.getPositionForCharacterIndex(characterIndex + 1); 63 | } 64 | 65 | ASSERT_EQ(LINE_COUNT(), 12); 66 | ASSERT_EQ(TEXT(), "a b c d e\nf g h i j\nk l m n o\np q r s t\nu v w x y\nz\nA B C D E\nF G H I J\nK L M N O\nP Q R S T\nU V W X Y\nZ"); 67 | } 68 | -------------------------------------------------------------------------------- /sources/tests/utf8.test.cc: -------------------------------------------------------------------------------- 1 | #include "./framework.hh" 2 | 3 | TEST_CASE("it should read graphemes as one") 4 | { 5 | SETUP("🇫🇷"); 6 | 7 | ASSERT_EQ(LINE_COUNT(), 1); 8 | ASSERT_EQ(LINE_SLICE(0, 0, 1), "🇫🇷"); 9 | } 10 | 11 | TEST_CASE("it should read grapheme clusters as one") 12 | { 13 | SETUP("Z̸̞̯̫̬̠̻̲̹̦̖͌̈́̿͒́̈́͛͘͠ạ̶̢͕̺̿̈́̑̈̅̎̈́͌l̵̡̮͉̯͉̘̘̯̖͍̫̖̗͖̩͈͑̄͂̓͗̾̆g̵̛͙̪͙͔͋̊́̉͌̾͌̃́̅̔̊͝o̵̭̗̮̬̪̫̗̊̈́̾̀̏̆̈́"); 14 | 15 | ASSERT_EQ(LINE_COUNT(), 1); 16 | ASSERT_EQ(LINE_SLICE(0, 0, 1), "Z̸̞̯̫̬̠̻̲̹̦̖͌̈́̿͒́̈́͛͘͠"); 17 | } 18 | -------------------------------------------------------------------------------- /sources/tools/TextOutput.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "./../TextOperation.hh" 6 | #include "./TextOutput.hh" 7 | 8 | TextOutput::TextOutput(void) 9 | : m_lines{ "" } 10 | { 11 | } 12 | 13 | TextOutput::TextOutput(std::vector const & lines) 14 | : m_lines(lines) 15 | { 16 | } 17 | 18 | unsigned TextOutput::getLineCount(void) const 19 | { 20 | return m_lines.size(); 21 | } 22 | 23 | std::string const & TextOutput::getLineForRow(unsigned row) const 24 | { 25 | assert(row < m_lines.size()); 26 | 27 | return m_lines.at(row); 28 | } 29 | 30 | std::string TextOutput::getText(void) const 31 | { 32 | std::ostringstream stream(m_lines.front(), std::ios_base::out | std::ios_base::ate); 33 | 34 | for (unsigned t = 1; t < m_lines.size(); ++t) 35 | stream << "\n" << m_lines.at(t); 36 | 37 | return stream.str(); 38 | } 39 | 40 | void TextOutput::apply(TextOperation const & textOperation) 41 | { 42 | assert(textOperation.startingRow <= m_lines.size()); 43 | assert(textOperation.startingRow + textOperation.deletedLineCount <= m_lines.size()); 44 | 45 | m_lines.erase(m_lines.begin() + textOperation.startingRow, m_lines.begin() + textOperation.startingRow + textOperation.deletedLineCount); 46 | 47 | for (unsigned t = 0; t < textOperation.addedLineCount; ++t) { 48 | m_lines.insert(m_lines.begin() + textOperation.startingRow + t, textOperation.addedLines.at(t).string.toString()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sources/tools/TextOutput.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "../TextOperation.hh" 7 | 8 | class TextOutput { 9 | 10 | public: 11 | 12 | TextOutput(void); 13 | TextOutput(std::vector const & lines); 14 | 15 | public: 16 | 17 | unsigned getLineCount(void) const; 18 | 19 | public: 20 | 21 | std::string const & getLineForRow(unsigned row) const; 22 | std::string getText(void) const; 23 | 24 | public: 25 | 26 | void apply(TextOperation const & textOperation); 27 | 28 | private: 29 | 30 | std::vector m_lines; 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "@types/zen-observable@npm:^0.8.3": 9 | version: 0.8.3 10 | resolution: "@types/zen-observable@npm:0.8.3" 11 | checksum: 10c0/c0605d109e58a32c9b47ab9becb4ee4bcd8ed54f452ccdcfbb025a60eb8abb1341f00fb045caaa6f1a72f1299f2cdf7b7918023aef34bd9bfdfdbae0e21e66eb 12 | languageName: node 13 | linkType: hard 14 | 15 | "balanced-match@npm:^0.4.1": 16 | version: 0.4.2 17 | resolution: "balanced-match@npm:0.4.2" 18 | checksum: 10c0/cd4e15add0f4ef14c4fe960d9f4a343052d7c0f7939e1b5e54c8f24417a501bde1f17e191b676daebd16ae316955c918f93b8ed0414bb03d038dd0159c9998e5 19 | languageName: node 20 | linkType: hard 21 | 22 | "benchmark@npm:^2.1.4": 23 | version: 2.1.4 24 | resolution: "benchmark@npm:2.1.4" 25 | dependencies: 26 | lodash: "npm:^4.17.4" 27 | platform: "npm:^1.3.3" 28 | checksum: 10c0/510224c01f7578e9aa60cef67ec3dd8f84ac6670007bcc96285f87865375122aca0853ab4e542cc80cfeeed436356dfdd63bb66cb5e72365abb912685b2139be 29 | languageName: node 30 | linkType: hard 31 | 32 | "brace-expansion@npm:^1.0.0": 33 | version: 1.1.6 34 | resolution: "brace-expansion@npm:1.1.6" 35 | dependencies: 36 | balanced-match: "npm:^0.4.1" 37 | concat-map: "npm:0.0.1" 38 | checksum: 10c0/44049645ad86d6b55f4aae8c3394216e69d731070c2c296ae13133096bcbbb6d3070d1df48b4839a10cd785c705229294a8bc208f37a0ba15427d762ecdd0951 39 | languageName: node 40 | linkType: hard 41 | 42 | "color-diff@npm:^1.2.0": 43 | version: 1.2.0 44 | resolution: "color-diff@npm:1.2.0" 45 | checksum: 10c0/1be1b3d97887bf63ad262da1b852715e09dc8922b4669275f8e7c7a9c921b20cac4e1cb97f3d00c60dde8917e7be73eb6ec6a2bc7571fec4291cb0196ca0371b 46 | languageName: node 47 | linkType: hard 48 | 49 | "concat-map@npm:0.0.1": 50 | version: 0.0.1 51 | resolution: "concat-map@npm:0.0.1" 52 | checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f 53 | languageName: node 54 | linkType: hard 55 | 56 | "faker@npm:^4.1.0": 57 | version: 4.1.0 58 | resolution: "faker@npm:4.1.0" 59 | checksum: 10c0/eaa96a7db715451128c4641c6df148b53ac80b863a64a16e9d7d416dd7b1fac53b5d02a300a3d36168467edb8a4ce2a169af2d820df7915ed0732044d997a50a 60 | languageName: node 61 | linkType: hard 62 | 63 | "fs.realpath@npm:^1.0.0": 64 | version: 1.0.0 65 | resolution: "fs.realpath@npm:1.0.0" 66 | checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 67 | languageName: node 68 | linkType: hard 69 | 70 | "glob@npm:^7.1.1": 71 | version: 7.1.1 72 | resolution: "glob@npm:7.1.1" 73 | dependencies: 74 | fs.realpath: "npm:^1.0.0" 75 | inflight: "npm:^1.0.4" 76 | inherits: "npm:2" 77 | minimatch: "npm:^3.0.2" 78 | once: "npm:^1.3.0" 79 | path-is-absolute: "npm:^1.0.0" 80 | checksum: 10c0/d41f501c68251a825724cd4aeea551a4bd8d216eb821e952f3c400066d18b744f775ad0d1649bdaaded7a5168e70d8cd308b432f0e5829d3143b28121b81f031 81 | languageName: node 82 | linkType: hard 83 | 84 | "inflight@npm:^1.0.4": 85 | version: 1.0.6 86 | resolution: "inflight@npm:1.0.6" 87 | dependencies: 88 | once: "npm:^1.3.0" 89 | wrappy: "npm:1" 90 | checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 91 | languageName: node 92 | linkType: hard 93 | 94 | "inherits@npm:2": 95 | version: 2.0.3 96 | resolution: "inherits@npm:2.0.3" 97 | checksum: 10c0/6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 98 | languageName: node 99 | linkType: hard 100 | 101 | "lodash@npm:^4.17.4": 102 | version: 4.17.21 103 | resolution: "lodash@npm:4.17.21" 104 | checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c 105 | languageName: node 106 | linkType: hard 107 | 108 | "minimatch@npm:^3.0.2": 109 | version: 3.0.3 110 | resolution: "minimatch@npm:3.0.3" 111 | dependencies: 112 | brace-expansion: "npm:^1.0.0" 113 | checksum: 10c0/827dcf6d4eb80c5d8a7bdcc5f88ef1c2c35e5d858122effb6bd83965e261417b9a559d3d33332b99ca98ddb7488d435e13046a1f8d6635245b906c59ee0f1185 114 | languageName: node 115 | linkType: hard 116 | 117 | "mono-layout@workspace:.": 118 | version: 0.0.0-use.local 119 | resolution: "mono-layout@workspace:." 120 | dependencies: 121 | benchmark: "npm:^2.1.4" 122 | faker: "npm:^4.1.0" 123 | glob: "npm:^7.1.1" 124 | term-strings: "npm:^0.14.1" 125 | languageName: unknown 126 | linkType: soft 127 | 128 | "once@npm:^1.3.0": 129 | version: 1.4.0 130 | resolution: "once@npm:1.4.0" 131 | dependencies: 132 | wrappy: "npm:1" 133 | checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 134 | languageName: node 135 | linkType: hard 136 | 137 | "path-is-absolute@npm:^1.0.0": 138 | version: 1.0.1 139 | resolution: "path-is-absolute@npm:1.0.1" 140 | checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 141 | languageName: node 142 | linkType: hard 143 | 144 | "platform@npm:^1.3.3": 145 | version: 1.3.6 146 | resolution: "platform@npm:1.3.6" 147 | checksum: 10c0/69f2eb692e15f1a343dd0d9347babd9ca933824c8673096be746ff66f99f2bdc909fadd8609076132e6ec768349080babb7362299f2a7f885b98f1254ae6224b 148 | languageName: node 149 | linkType: hard 150 | 151 | "term-strings@npm:^0.14.1": 152 | version: 0.14.1 153 | resolution: "term-strings@npm:0.14.1" 154 | dependencies: 155 | "@types/zen-observable": "npm:^0.8.3" 156 | color-diff: "npm:^1.2.0" 157 | zen-observable: "npm:^0.8.15" 158 | bin: 159 | term-strings: ./build/bin/term-strings.js 160 | term-strings-seqdbg: ./build/bin/term-strings-seqdbg.js 161 | checksum: 10c0/5509ad22355712ad65483aa33a94645ce48725f4160045847d16285bb4af75fc523b5dc79bd641d812e20445ec7dcfb543a32c4291adda911728b2458a4ecc9f 162 | languageName: node 163 | linkType: hard 164 | 165 | "wrappy@npm:1": 166 | version: 1.0.2 167 | resolution: "wrappy@npm:1.0.2" 168 | checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 169 | languageName: node 170 | linkType: hard 171 | 172 | "zen-observable@npm:^0.8.15": 173 | version: 0.8.15 174 | resolution: "zen-observable@npm:0.8.15" 175 | checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8 176 | languageName: node 177 | linkType: hard 178 | --------------------------------------------------------------------------------