Skip to content

Commit

Permalink
Merge pull request #8 from cto-af/string-width
Browse files Browse the repository at this point in the history
String width
  • Loading branch information
hildjj authored Jun 17, 2023
2 parents 8d6f7dd + a37ce16 commit b5b1a2b
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 128 deletions.
2 changes: 1 addition & 1 deletion docs/assets/search.js

Large diffs are not rendered by default.

115 changes: 66 additions & 49 deletions docs/classes/index.LineWrap.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/classes/spacebreaker.SpaceBreaker.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ <h4>Hierarchy</h4>
<ul class="tsd-hierarchy">
<li><span class="target">SpaceBreaker</span></li></ul></li></ul></section><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/68c9344/lib/spacebreaker.js#L138">lib/spacebreaker.js:138</a></li></ul></aside>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/b63e3d6/lib/spacebreaker.js#L138">lib/spacebreaker.js:138</a></li></ul></aside>
<section class="tsd-panel-group tsd-index-group">
<section class="tsd-panel tsd-index-panel">
<details class="tsd-index-content tsd-index-accordion" open><summary class="tsd-accordion-summary tsd-index-summary">
Expand Down Expand Up @@ -82,7 +82,7 @@ <h4 class="tsd-returns-title">Returns <a href="spacebreaker.SpaceBreaker.html" c
<div class="tsd-comment tsd-typography"></div><aside class="tsd-sources">
<p>Overrides Rules.constructor</p>
<ul>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/68c9344/lib/spacebreaker.js#L143">lib/spacebreaker.js:143</a></li></ul></aside></li></ul></section></section>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/b63e3d6/lib/spacebreaker.js#L143">lib/spacebreaker.js:143</a></li></ul></aside></li></ul></section></section>
<section class="tsd-panel-group tsd-member-group">
<h2>Properties</h2>
<section class="tsd-panel tsd-member tsd-is-private tsd-is-inherited tsd-is-external"><a id="_private" class="tsd-anchor"></a>
Expand Down
2 changes: 2 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h2>@cto.af/linewrap</h2></div>
<div class="tsd-panel tsd-typography"><a id="md:ctoaflinewrap" class="tsd-anchor"></a><h1><a href="#md:ctoaflinewrap">@cto.af/linewrap</a></h1><p>Wrap lines using Unicode UAX #14 line breaking rules.</p>
<a id="md:installation" class="tsd-anchor"></a><h2><a href="#md:installation">Installation</a></h2><pre><code class="language-sh"><span class="hl-0">npm</span><span class="hl-1"> </span><span class="hl-2">install</span><span class="hl-1"> </span><span class="hl-2">@cto.af/linewrap</span>
</code><button>Copy</button></pre>
<a id="md:cli" class="tsd-anchor"></a><h2><a href="#md:cli">CLI</a></h2><p>A command line interface is available: <a href="https://github.com/cto-af/linewrap-cli">@cto.af/linewrap-cli</a></p>
<a id="md:api" class="tsd-anchor"></a><h2><a href="#md:api">API</a></h2><pre><code class="language-js"><span class="hl-3">import</span><span class="hl-1"> {</span><span class="hl-4">LineWrap</span><span class="hl-1">} </span><span class="hl-3">from</span><span class="hl-1"> </span><span class="hl-2">&#39;@cto.af/linewrap&#39;</span><br/><span class="hl-5">const</span><span class="hl-1"> </span><span class="hl-6">w</span><span class="hl-1"> = </span><span class="hl-5">new</span><span class="hl-1"> </span><span class="hl-0">LineWrap</span><span class="hl-1">()</span><br/><span class="hl-4">w</span><span class="hl-1">.</span><span class="hl-0">wrap</span><span class="hl-1">(</span><span class="hl-2">&#39;Lorem ipsum dolor sit amet...&#39;</span><span class="hl-1">) </span><span class="hl-7">// A string, wrapped to your console length</span><br/><span class="hl-3">for</span><span class="hl-1"> (</span><span class="hl-5">const</span><span class="hl-1"> </span><span class="hl-6">line</span><span class="hl-1"> </span><span class="hl-5">of</span><span class="hl-1"> </span><span class="hl-4">w</span><span class="hl-1">.</span><span class="hl-0">lines</span><span class="hl-1">(</span><span class="hl-2">&#39;Lorem ipsum dolor sit amet...&#39;</span><span class="hl-1">)) {</span><br/><span class="hl-1"> </span><span class="hl-7">// `line` does not have a newline at the end</span><br/><span class="hl-1">}</span>
</code><button>Copy</button></pre>
<p>Full <a href="https://cto-af.github.io/linewrap/">API docs</a> are available.</p>
Expand Down Expand Up @@ -145,6 +146,7 @@ <h3><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><use href="#icon
<li>
<ul>
<li><a href="#md:installation"><span>Installation</span></a></li>
<li><a href="#md:cli"><span>CLI</span></a></li>
<li><a href="#md:api"><span>API</span></a></li>
<li>
<ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<li><a href="index.html">index</a></li></ul>
<h1>Module index</h1></div><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/68c9344/lib/index.js#L1">lib/index.js:1</a></li></ul></aside>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/b63e3d6/lib/index.js#L1">lib/index.js:1</a></li></ul></aside>
<section class="tsd-panel-group tsd-index-group">
<section class="tsd-panel tsd-index-panel">
<h3 class="tsd-index-heading uppercase">Index</h3>
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/spacebreaker.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<li><a href="spacebreaker.html">spacebreaker</a></li></ul>
<h1>Module spacebreaker</h1></div><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/68c9344/lib/spacebreaker.js#L1">lib/spacebreaker.js:1</a></li></ul></aside>
<li>Defined in <a href="https://github.com/cto-af/linewrap/blob/b63e3d6/lib/spacebreaker.js#L1">lib/spacebreaker.js:1</a></li></ul></aside>
<section class="tsd-panel-group tsd-index-group">
<section class="tsd-panel tsd-index-panel">
<h3 class="tsd-index-heading uppercase">Index</h3>
Expand Down
101 changes: 60 additions & 41 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Break} from '@cto.af/linebreak'
import {SpaceBreaker} from './spacebreaker.js'
import {StringWidth} from '@cto.af/string-width'

const DEFAULT_LOCALE = new Intl.Segmenter().resolvedOptions().locale
const DEFAULT_IS_NEWLINE = /[^\S\r\n\v\f\x85\u2028\u2029]*[\r\n\v\f\x85\u2028\u2029]+\s*/gu
Expand Down Expand Up @@ -40,7 +41,7 @@ export class LineWrap {
*/
static OVERFLOW_ANYWHERE = Symbol('overflow-clip')

#graphemes
#stringWidth
#rules
#opts

Expand All @@ -57,19 +58,19 @@ export class LineWrap {
#indent

/**
* Length of indent in graphemes
* Length of indent in display cells
* @type {number}
*/
#indentWidth

/**
* Width of #opts.newlineReplacement, in graphemes
* Width of #opts.newlineReplacement, in display cells
* @type {number}
*/
#replacementWidth

/**
* Width of #opts.ellipsis or #opts.hyphen, in graphemes
* Width of #opts.ellipsis or #opts.hyphen, in display cells
* @type {number}
*/
#enderWidth
Expand All @@ -88,7 +89,7 @@ export class LineWrap {
* Example 7 of UAX #14.
* @prop {number} [firstCol=NaN] If indentFirst is false, how many columns
* was the first line already indented? If NaN, use the indent width,
* in graphemes. If indentFirst is true, this is ignored.
* in display cells. If indentFirst is true, this is ignored.
* @prop {string} [hyphen='-'] String to use when long word is
* split to next line with LineWrap.OVERFLOW_ANYWHERE.
* @prop {number|string} [indent=''] If a string, indent every line with
Expand All @@ -101,6 +102,7 @@ export class LineWrap {
* @prop {boolean} [indentFirst=true] Indent the first line? If not, treat
* the first line as if it was already indented, giving a short first
* line.
* @prop {boolean} [isCJK] If specified, override CJK detection by locale.
* @prop {RegExp} [isNewline=DEFAULT_IS_NEWLINE] Regular expression that
* finds newlines for replacement with `newlineReplacement`. Ensure you
* do not create a regular expression denial of service
Expand Down Expand Up @@ -139,6 +141,7 @@ export class LineWrap {
indentChar: ' ',
indentEmpty: false,
indentFirst: true,
isCJK: false,
isNewline: DEFAULT_IS_NEWLINE,
locale: DEFAULT_LOCALE,
newline: '\n',
Expand All @@ -163,9 +166,21 @@ export class LineWrap {
example7: this.#opts.example7,
verbose: this.#opts.verbose,
})
this.#graphemes = new Intl.Segmenter(this.#opts.locale, {
granularity: 'grapheme',
})

/**
* Don't use this.#opts because it's polluted by the default
* @type {{locale?: string, isCJK?: boolean}}
*/
const swOpts = {}
if (typeof opts.locale === 'string') {
swOpts.locale = opts.locale
}
if (typeof opts.isCJK === 'boolean') {
swOpts.isCJK = opts.isCJK
}
this.#stringWidth = new StringWidth(swOpts)
this.#opts.locale = this.#stringWidth.locale
this.#opts.isCJK = this.#stringWidth.isCJK

if (typeof this.#opts.indent === 'number') {
this.#indent = ''.padEnd(
Expand All @@ -175,7 +190,7 @@ export class LineWrap {
this.#indentWidth = this.#opts.indent
} else {
this.#indent = this.#opts.indent
this.#indentWidth = this.#graphemeCount(this.#indent)
this.#indentWidth = this.#stringWidth.width(this.#indent)
}
this.#firstIndent = (this.#opts.indentFirst || isNaN(this.#opts.firstCol)) ?
this.#indentWidth :
Expand All @@ -185,16 +200,18 @@ export class LineWrap {
if (this.#workingWidth <= 0) {
throw new Error(`No space to wrap, incompatible width and indent: ${this.#workingWidth}`)
}
this.#replacementWidth = this.#graphemeCount(this.#opts.newlineReplacement)
this.#replacementWidth = this.#stringWidth.width(
this.#opts.newlineReplacement
)
switch (this.#opts.overflow) {
case LineWrap.OVERFLOW_VISIBLE:
this.#enderWidth = 0
break
case LineWrap.OVERFLOW_CLIP:
this.#enderWidth = this.#graphemeCount(this.#opts.ellipsis)
this.#enderWidth = this.#stringWidth.width(this.#opts.ellipsis)
break
case LineWrap.OVERFLOW_ANYWHERE:
this.#enderWidth = this.#graphemeCount(this.#opts.hyphen)
this.#enderWidth = this.#stringWidth.width(this.#opts.hyphen)
break
default:
throw new Error(`Invalid overflow style: "${String(this.#opts.overflow)}"`)
Expand All @@ -205,18 +222,17 @@ export class LineWrap {
}

/**
* How many graphemes are in this string?
*
* @param {string} str
* @returns {number}
* Did we determin that we are in a CJK context? Useful for testing.
*/
#graphemeCount(str) {
// TODO: count widths better, including ZW and ea=F
let ret = 0
for (const _ of this.#graphemes.segment(str)) {
ret++
}
return ret
get isCJK() {
return this.#opts.isCJK
}

/**
* The calculated locale. Useful for testing.
*/
get locale() {
return this.#opts.locale
}

/**
Expand All @@ -228,7 +244,7 @@ export class LineWrap {
*/
*#fragments(brk, first) {
const seg = /** @type {string} */ (brk.string)
const graphemes = this.#graphemeCount(seg)
const graphemes = this.#stringWidth.width(seg)
const width = (first && !this.#opts.indentFirst) ?
this.#opts.width - this.#firstIndent :
this.#workingWidth
Expand Down Expand Up @@ -263,31 +279,34 @@ export class LineWrap {
case LineWrap.OVERFLOW_CLIP: {
// Clip it, and end with an ellipsis
const b = new Break(-1, false)
b.string = [...this.#graphemes.segment(seg)]
.slice(0, width - this.#enderWidth)
.map(s => s.segment)
.join('') +
this.#opts.ellipsis
b.props = {...brk.props, graphemes: width}
const breaks = this.#stringWidth.break(
seg,
width - this.#enderWidth
)
// Note that breaks[0].cells might be less than width - enderWidth
// assert(breaks.length > 0)
// assert(!breaks[0].last)
b.string = breaks[0].string + this.#opts.ellipsis
b.props = {...brk.props, graphemes: breaks[0].cells + this.#enderWidth}
yield b
break
}
case LineWrap.OVERFLOW_ANYWHERE: {
const g = [...this.#graphemes.segment(seg)]
const page = width - this.#enderWidth
// Might be more that one line long.
for (let offset = 0; offset < g.length; offset += page) {
const breaks = this.#stringWidth.break(
seg,
width - this.#enderWidth
)
for (const {string, cells, last} of breaks) {
const b = new Break(-1, false)
const pg = g.slice(offset, offset + page)
b.string = pg.map(s => s.segment).join('')
b.props = {...brk.props, graphemes: pg.length}
if (offset + page < g.length) {
b.string += this.#opts.hyphen
b.props.graphemes += this.#enderWidth
if (last) {
b.string = string
b.props = {...brk.props, graphemes: cells}
} else {
b.string = string + this.#opts.hyphen
b.props = {...brk.props, graphemes: cells + this.#enderWidth}
}
yield b
}
break
}
}
}
Expand Down
Loading

0 comments on commit b5b1a2b

Please sign in to comment.