vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

657 lines
16 KiB

Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
  1. let LabelAccumulator = require('./LabelAccumulator').default;
  2. let ComponentUtil = require('./ComponentUtil').default;
  3. // Hash of prepared regexp's for tags
  4. var tagPattern = {
  5. // HTML
  6. '<b>': /<b>/,
  7. '<i>': /<i>/,
  8. '<code>': /<code>/,
  9. '</b>': /<\/b>/,
  10. '</i>': /<\/i>/,
  11. '</code>': /<\/code>/,
  12. // Markdown
  13. '*': /\*/, // bold
  14. '_': /\_/, // ital
  15. '`': /`/, // mono
  16. 'afterBold': /[^\*]/,
  17. 'afterItal': /[^_]/,
  18. 'afterMono': /[^`]/,
  19. };
  20. /**
  21. * Internal helper class for parsing the markup tags for HTML and Markdown.
  22. *
  23. * NOTE: Sequences of tabs and spaces are reduced to single space.
  24. * Scan usage of `this.spacing` within method
  25. */
  26. class MarkupAccumulator {
  27. /**
  28. * Create an instance
  29. *
  30. * @param {string} text text to parse for markup
  31. */
  32. constructor(text) {
  33. this.text = text;
  34. this.bold = false;
  35. this.ital = false;
  36. this.mono = false;
  37. this.spacing = false;
  38. this.position = 0;
  39. this.buffer = "";
  40. this.modStack = [];
  41. this.blocks = [];
  42. }
  43. /**
  44. * Return the mod label currently on the top of the stack
  45. *
  46. * @returns {string} label of topmost mod
  47. * @private
  48. */
  49. mod() {
  50. return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
  51. }
  52. /**
  53. * Return the mod label currently active
  54. *
  55. * @returns {string} label of active mod
  56. * @private
  57. */
  58. modName() {
  59. if (this.modStack.length === 0)
  60. return 'normal';
  61. else if (this.modStack[0] === 'mono')
  62. return 'mono';
  63. else {
  64. if (this.bold && this.ital) {
  65. return 'boldital';
  66. } else if (this.bold) {
  67. return 'bold';
  68. } else if (this.ital) {
  69. return 'ital';
  70. }
  71. }
  72. }
  73. /**
  74. * @private
  75. */
  76. emitBlock() {
  77. if (this.spacing) {
  78. this.add(" ");
  79. this.spacing = false;
  80. }
  81. if (this.buffer.length > 0) {
  82. this.blocks.push({ text: this.buffer, mod: this.modName() });
  83. this.buffer = "";
  84. }
  85. }
  86. /**
  87. * Output text to buffer
  88. *
  89. * @param {string} text text to add
  90. * @private
  91. */
  92. add(text) {
  93. if (text === " ") {
  94. this.spacing = true;
  95. }
  96. if (this.spacing) {
  97. this.buffer += " ";
  98. this.spacing = false;
  99. }
  100. if (text != " ") {
  101. this.buffer += text;
  102. }
  103. }
  104. /**
  105. * Handle parsing of whitespace
  106. *
  107. * @param {string} ch the character to check
  108. * @returns {boolean} true if the character was processed as whitespace, false otherwise
  109. */
  110. parseWS(ch) {
  111. if (/[ \t]/.test(ch)) {
  112. if (!this.mono) {
  113. this.spacing = true;
  114. } else {
  115. this.add(ch);
  116. }
  117. return true;
  118. }
  119. return false;
  120. }
  121. /**
  122. * @param {string} tagName label for block type to set
  123. * @private
  124. */
  125. setTag(tagName) {
  126. this.emitBlock();
  127. this[tagName] = true;
  128. this.modStack.unshift(tagName);
  129. }
  130. /**
  131. * @param {string} tagName label for block type to unset
  132. * @private
  133. */
  134. unsetTag(tagName) {
  135. this.emitBlock();
  136. this[tagName] = false;
  137. this.modStack.shift();
  138. }
  139. /**
  140. * @param {string} tagName label for block type we are currently processing
  141. * @param {string|RegExp} tag string to match in text
  142. * @returns {boolean} true if the tag was processed, false otherwise
  143. */
  144. parseStartTag(tagName, tag) {
  145. // Note: if 'mono' passed as tagName, there is a double check here. This is OK
  146. if (!this.mono && !this[tagName] && this.match(tag)) {
  147. this.setTag(tagName);
  148. return true;
  149. }
  150. return false;
  151. }
  152. /**
  153. * @param {string|RegExp} tag
  154. * @param {number} [advance=true] if set, advance current position in text
  155. * @returns {boolean} true if match at given position, false otherwise
  156. * @private
  157. */
  158. match(tag, advance = true) {
  159. let [regExp, length] = this.prepareRegExp(tag);
  160. let matched = regExp.test(this.text.substr(this.position, length));
  161. if (matched && advance) {
  162. this.position += length - 1;
  163. }
  164. return matched;
  165. }
  166. /**
  167. * @param {string} tagName label for block type we are currently processing
  168. * @param {string|RegExp} tag string to match in text
  169. * @param {RegExp} [nextTag] regular expression to match for characters *following* the current tag
  170. * @returns {boolean} true if the tag was processed, false otherwise
  171. */
  172. parseEndTag(tagName, tag, nextTag) {
  173. let checkTag = (this.mod() === tagName);
  174. if (tagName === 'mono') { // special handling for 'mono'
  175. checkTag = checkTag && this.mono;
  176. } else {
  177. checkTag = checkTag && !this.mono;
  178. }
  179. if (checkTag && this.match(tag)) {
  180. if (nextTag !== undefined) {
  181. // Purpose of the following match is to prevent a direct unset/set of a given tag
  182. // E.g. '*bold **still bold*' => '*bold still bold*'
  183. if ((this.position === this.text.length-1) || this.match(nextTag, false)) {
  184. this.unsetTag(tagName);
  185. }
  186. } else {
  187. this.unsetTag(tagName);
  188. }
  189. return true;
  190. }
  191. return false;
  192. }
  193. /**
  194. * @param {string|RegExp} tag string to match in text
  195. * @param {value} value string to replace tag with, if found at current position
  196. * @returns {boolean} true if the tag was processed, false otherwise
  197. */
  198. replace(tag, value) {
  199. if (this.match(tag)) {
  200. this.add(value);
  201. this.position += length - 1;
  202. return true;
  203. }
  204. return false;
  205. }
  206. /**
  207. * Create a regular expression for the tag if it isn't already one.
  208. *
  209. * @param {string|RegExp} tag string to match in text
  210. * @returns {[RegExp, number]} regular expression to use and length of input string to match
  211. * @private
  212. */
  213. prepareRegExp(tag) {
  214. let length;
  215. let regExp;
  216. if (tag instanceof RegExp) {
  217. regExp = tag;
  218. length = 1; // ASSUMPTION: regexp only tests one character
  219. } else {
  220. // use prepared regexp if present
  221. var prepared = tagPattern[tag];
  222. if (prepared !== undefined) {
  223. regExp = prepared;
  224. } else {
  225. regExp = new RegExp(tag);
  226. }
  227. length = tag.length;
  228. }
  229. return [regExp, length];
  230. }
  231. }
  232. /**
  233. * Helper class for Label which explodes the label text into lines and blocks within lines
  234. *
  235. * @private
  236. */
  237. class LabelSplitter {
  238. /**
  239. * @param {CanvasRenderingContext2D} ctx Canvas rendering context
  240. * @param {Label} parent reference to the Label instance using current instance
  241. * @param {boolean} selected
  242. * @param {boolean} hover
  243. */
  244. constructor(ctx, parent, selected, hover) {
  245. this.ctx = ctx;
  246. this.parent = parent;
  247. /**
  248. * Callback to determine text width; passed to LabelAccumulator instance
  249. *
  250. * @param {String} text string to determine width of
  251. * @param {String} mod font type to use for this text
  252. * @return {Object} { width, values} width in pixels and font attributes
  253. */
  254. let textWidth = (text, mod) => {
  255. if (text === undefined) return 0;
  256. // TODO: This can be done more efficiently with caching
  257. let values = this.parent.getFormattingValues(ctx, selected, hover, mod);
  258. let width = 0;
  259. if (text !== '') {
  260. // NOTE: The following may actually be *incorrect* for the mod fonts!
  261. // This returns the size with a regular font, bold etc. may
  262. // have different sizes.
  263. let measure = this.ctx.measureText(text);
  264. width = measure.width;
  265. }
  266. return {width, values: values};
  267. };
  268. this.lines = new LabelAccumulator(textWidth);
  269. }
  270. /**
  271. * Split passed text of a label into lines and blocks.
  272. *
  273. * # NOTE
  274. *
  275. * The handling of spacing is option dependent:
  276. *
  277. * - if `font.multi : false`, all spaces are retained
  278. * - if `font.multi : true`, every sequence of spaces is compressed to a single space
  279. *
  280. * This might not be the best way to do it, but this is as it has been working till now.
  281. * In order not to break existing functionality, for the time being this behaviour will
  282. * be retained in any code changes.
  283. *
  284. * @param {string} text text to split
  285. * @returns {Array<line>}
  286. */
  287. process(text) {
  288. if (!ComponentUtil.isValidLabel(text)) {
  289. return this.lines.finalize();
  290. }
  291. var font = this.parent.fontOptions;
  292. // Normalize the end-of-line's to a single representation - order important
  293. text = text.replace(/\r\n/g, '\n'); // Dos EOL's
  294. text = text.replace(/\r/g, '\n'); // Mac EOL's
  295. // Note that at this point, there can be no \r's in the text.
  296. // This is used later on splitStringIntoLines() to split multifont texts.
  297. let nlLines = String(text).split('\n');
  298. let lineCount = nlLines.length;
  299. if (font.multi) {
  300. // Multi-font case: styling tags active
  301. for (let i = 0; i < lineCount; i++) {
  302. let blocks = this.splitBlocks(nlLines[i], font.multi);
  303. // Post: Sequences of tabs and spaces are reduced to single space
  304. if (blocks === undefined) continue;
  305. if (blocks.length === 0) {
  306. this.lines.newLine("");
  307. continue;
  308. }
  309. if (font.maxWdt > 0) {
  310. // widthConstraint.maximum defined
  311. //console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt);
  312. for (let j = 0; j < blocks.length; j++) {
  313. let mod = blocks[j].mod;
  314. let text = blocks[j].text;
  315. this.splitStringIntoLines(text, mod, true);
  316. }
  317. } else {
  318. // widthConstraint.maximum NOT defined
  319. for (let j = 0; j < blocks.length; j++) {
  320. let mod = blocks[j].mod;
  321. let text = blocks[j].text;
  322. this.lines.append(text, mod);
  323. }
  324. }
  325. this.lines.newLine();
  326. }
  327. } else {
  328. // Single-font case
  329. if (font.maxWdt > 0) {
  330. // widthConstraint.maximum defined
  331. // console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt);
  332. for (let i = 0; i < lineCount; i++) {
  333. this.splitStringIntoLines(nlLines[i]);
  334. }
  335. } else {
  336. // widthConstraint.maximum NOT defined
  337. for (let i = 0; i < lineCount; i++) {
  338. this.lines.newLine(nlLines[i]);
  339. }
  340. }
  341. }
  342. return this.lines.finalize();
  343. }
  344. /**
  345. * normalize the markup system
  346. *
  347. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  348. * @returns {string}
  349. */
  350. decodeMarkupSystem(markupSystem) {
  351. let system = 'none';
  352. if (markupSystem === 'markdown' || markupSystem === 'md') {
  353. system = 'markdown';
  354. } else if (markupSystem === true || markupSystem === 'html') {
  355. system = 'html'
  356. }
  357. return system;
  358. }
  359. /**
  360. *
  361. * @param {string} text
  362. * @returns {Array}
  363. */
  364. splitHtmlBlocks(text) {
  365. let s = new MarkupAccumulator(text);
  366. let parseEntities = (ch) => {
  367. if (/&/.test(ch)) {
  368. let parsed = s.replace(s.text, '&lt;', '<')
  369. || s.replace(s.text, '&amp;', '&');
  370. if (!parsed) {
  371. s.add("&");
  372. }
  373. return true;
  374. }
  375. return false;
  376. };
  377. while (s.position < s.text.length) {
  378. let ch = s.text.charAt(s.position);
  379. let parsed = s.parseWS(ch)
  380. || (/</.test(ch) && (
  381. s.parseStartTag('bold', '<b>')
  382. || s.parseStartTag('ital', '<i>')
  383. || s.parseStartTag('mono', '<code>')
  384. || s.parseEndTag('bold', '</b>')
  385. || s.parseEndTag('ital', '</i>')
  386. || s.parseEndTag('mono', '</code>')))
  387. || parseEntities(ch);
  388. if (!parsed) {
  389. s.add(ch);
  390. }
  391. s.position++
  392. }
  393. s.emitBlock();
  394. return s.blocks;
  395. }
  396. /**
  397. *
  398. * @param {string} text
  399. * @returns {Array}
  400. */
  401. splitMarkdownBlocks(text) {
  402. let s = new MarkupAccumulator(text);
  403. let beginable = true;
  404. let parseOverride = (ch) => {
  405. if (/\\/.test(ch)) {
  406. if (s.position < this.text.length + 1) {
  407. s.position++;
  408. ch = this.text.charAt(s.position);
  409. if (/ \t/.test(ch)) {
  410. s.spacing = true;
  411. } else {
  412. s.add(ch);
  413. beginable = false;
  414. }
  415. }
  416. return true
  417. }
  418. return false;
  419. }
  420. while (s.position < s.text.length) {
  421. let ch = s.text.charAt(s.position);
  422. let parsed = s.parseWS(ch)
  423. || parseOverride(ch)
  424. || ((beginable || s.spacing) && (
  425. s.parseStartTag('bold', '*')
  426. || s.parseStartTag('ital', '_')
  427. || s.parseStartTag('mono', '`')))
  428. || s.parseEndTag('bold', '*', 'afterBold')
  429. || s.parseEndTag('ital', '_', 'afterItal')
  430. || s.parseEndTag('mono', '`', 'afterMono');
  431. if (!parsed) {
  432. s.add(ch);
  433. beginable = false;
  434. }
  435. s.position++
  436. }
  437. s.emitBlock();
  438. return s.blocks;
  439. }
  440. /**
  441. * Explodes a piece of text into single-font blocks using a given markup
  442. *
  443. * @param {string} text
  444. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  445. * @returns {Array.<{text: string, mod: string}>}
  446. * @private
  447. */
  448. splitBlocks(text, markupSystem) {
  449. let system = this.decodeMarkupSystem(markupSystem);
  450. if (system === 'none') {
  451. return [{
  452. text: text,
  453. mod: 'normal'
  454. }]
  455. } else if (system === 'markdown') {
  456. return this.splitMarkdownBlocks(text);
  457. } else if (system === 'html') {
  458. return this.splitHtmlBlocks(text);
  459. }
  460. }
  461. /**
  462. * @param {string} text
  463. * @returns {boolean} true if text length over the current max with
  464. * @private
  465. */
  466. overMaxWidth(text) {
  467. let width = this.ctx.measureText(text).width;
  468. return (this.lines.curWidth() + width > this.parent.fontOptions.maxWdt);
  469. }
  470. /**
  471. * Determine the longest part of the sentence which still fits in the
  472. * current max width.
  473. *
  474. * @param {Array} words Array of strings signifying a text lines
  475. * @return {number} index of first item in string making string go over max
  476. * @private
  477. */
  478. getLongestFit(words) {
  479. let text = '';
  480. let w = 0;
  481. while (w < words.length) {
  482. let pre = (text === '') ? '' : ' ';
  483. let newText = text + pre + words[w];
  484. if (this.overMaxWidth(newText)) break;
  485. text = newText;
  486. w++;
  487. }
  488. return w;
  489. }
  490. /**
  491. * Determine the longest part of the string which still fits in the
  492. * current max width.
  493. *
  494. * @param {Array} words Array of strings signifying a text lines
  495. * @return {number} index of first item in string making string go over max
  496. */
  497. getLongestFitWord(words) {
  498. let w = 0;
  499. while (w < words.length) {
  500. if (this.overMaxWidth(words.slice(0,w))) break;
  501. w++;
  502. }
  503. return w;
  504. }
  505. /**
  506. * Split the passed text into lines, according to width constraint (if any).
  507. *
  508. * The method assumes that the input string is a single line, i.e. without lines break.
  509. *
  510. * This method retains spaces, if still present (case `font.multi: false`).
  511. * A space which falls on an internal line break, will be replaced by a newline.
  512. * There is no special handling of tabs; these go along with the flow.
  513. *
  514. * @param {string} str
  515. * @param {string} [mod='normal']
  516. * @param {boolean} [appendLast=false]
  517. * @private
  518. */
  519. splitStringIntoLines(str, mod = 'normal', appendLast = false) {
  520. // Still-present spaces are relevant, retain them
  521. str = str.replace(/^( +)/g, '$1\r');
  522. str = str.replace(/([^\r][^ ]*)( +)/g, '$1\r$2\r');
  523. let words = str.split('\r');
  524. while (words.length > 0) {
  525. let w = this.getLongestFit(words);
  526. if (w === 0) {
  527. // Special case: the first word is already larger than the max width.
  528. let word = words[0];
  529. // Break the word to the largest part that fits the line
  530. let x = this.getLongestFitWord(word);
  531. this.lines.newLine(word.slice(0, x), mod);
  532. // Adjust the word, so that the rest will be done next iteration
  533. words[0] = word.slice(x);
  534. } else {
  535. // skip any space that is replaced by a newline
  536. let newW = w;
  537. if (words[w - 1] === ' ') {
  538. w--;
  539. } else if (words[newW] === ' ') {
  540. newW++;
  541. }
  542. let text = words.slice(0, w).join("");
  543. if (w == words.length && appendLast) {
  544. this.lines.append(text, mod);
  545. } else {
  546. this.lines.newLine(text, mod);
  547. }
  548. // Adjust the word, so that the rest will be done next iteration
  549. words = words.slice(newW);
  550. }
  551. }
  552. }
  553. }
  554. export default LabelSplitter;