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.

661 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. * The return value is an array `[RegExp, number]`, with exactly two value, where:
  210. * - RegExp is the regular expression to use
  211. * - number is the lenth of the input string to match
  212. *
  213. * @param {string|RegExp} tag string to match in text
  214. * @returns {Array} regular expression to use and length of input string to match
  215. * @private
  216. */
  217. prepareRegExp(tag) {
  218. let length;
  219. let regExp;
  220. if (tag instanceof RegExp) {
  221. regExp = tag;
  222. length = 1; // ASSUMPTION: regexp only tests one character
  223. } else {
  224. // use prepared regexp if present
  225. var prepared = tagPattern[tag];
  226. if (prepared !== undefined) {
  227. regExp = prepared;
  228. } else {
  229. regExp = new RegExp(tag);
  230. }
  231. length = tag.length;
  232. }
  233. return [regExp, length];
  234. }
  235. }
  236. /**
  237. * Helper class for Label which explodes the label text into lines and blocks within lines
  238. *
  239. * @private
  240. */
  241. class LabelSplitter {
  242. /**
  243. * @param {CanvasRenderingContext2D} ctx Canvas rendering context
  244. * @param {Label} parent reference to the Label instance using current instance
  245. * @param {boolean} selected
  246. * @param {boolean} hover
  247. */
  248. constructor(ctx, parent, selected, hover) {
  249. this.ctx = ctx;
  250. this.parent = parent;
  251. /**
  252. * Callback to determine text width; passed to LabelAccumulator instance
  253. *
  254. * @param {String} text string to determine width of
  255. * @param {String} mod font type to use for this text
  256. * @return {Object} { width, values} width in pixels and font attributes
  257. */
  258. let textWidth = (text, mod) => {
  259. if (text === undefined) return 0;
  260. // TODO: This can be done more efficiently with caching
  261. let values = this.parent.getFormattingValues(ctx, selected, hover, mod);
  262. let width = 0;
  263. if (text !== '') {
  264. // NOTE: The following may actually be *incorrect* for the mod fonts!
  265. // This returns the size with a regular font, bold etc. may
  266. // have different sizes.
  267. let measure = this.ctx.measureText(text);
  268. width = measure.width;
  269. }
  270. return {width, values: values};
  271. };
  272. this.lines = new LabelAccumulator(textWidth);
  273. }
  274. /**
  275. * Split passed text of a label into lines and blocks.
  276. *
  277. * # NOTE
  278. *
  279. * The handling of spacing is option dependent:
  280. *
  281. * - if `font.multi : false`, all spaces are retained
  282. * - if `font.multi : true`, every sequence of spaces is compressed to a single space
  283. *
  284. * This might not be the best way to do it, but this is as it has been working till now.
  285. * In order not to break existing functionality, for the time being this behaviour will
  286. * be retained in any code changes.
  287. *
  288. * @param {string} text text to split
  289. * @returns {Array<line>}
  290. */
  291. process(text) {
  292. if (!ComponentUtil.isValidLabel(text)) {
  293. return this.lines.finalize();
  294. }
  295. var font = this.parent.fontOptions;
  296. // Normalize the end-of-line's to a single representation - order important
  297. text = text.replace(/\r\n/g, '\n'); // Dos EOL's
  298. text = text.replace(/\r/g, '\n'); // Mac EOL's
  299. // Note that at this point, there can be no \r's in the text.
  300. // This is used later on splitStringIntoLines() to split multifont texts.
  301. let nlLines = String(text).split('\n');
  302. let lineCount = nlLines.length;
  303. if (font.multi) {
  304. // Multi-font case: styling tags active
  305. for (let i = 0; i < lineCount; i++) {
  306. let blocks = this.splitBlocks(nlLines[i], font.multi);
  307. // Post: Sequences of tabs and spaces are reduced to single space
  308. if (blocks === undefined) continue;
  309. if (blocks.length === 0) {
  310. this.lines.newLine("");
  311. continue;
  312. }
  313. if (font.maxWdt > 0) {
  314. // widthConstraint.maximum defined
  315. //console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt);
  316. for (let j = 0; j < blocks.length; j++) {
  317. let mod = blocks[j].mod;
  318. let text = blocks[j].text;
  319. this.splitStringIntoLines(text, mod, true);
  320. }
  321. } else {
  322. // widthConstraint.maximum NOT defined
  323. for (let j = 0; j < blocks.length; j++) {
  324. let mod = blocks[j].mod;
  325. let text = blocks[j].text;
  326. this.lines.append(text, mod);
  327. }
  328. }
  329. this.lines.newLine();
  330. }
  331. } else {
  332. // Single-font case
  333. if (font.maxWdt > 0) {
  334. // widthConstraint.maximum defined
  335. // console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt);
  336. for (let i = 0; i < lineCount; i++) {
  337. this.splitStringIntoLines(nlLines[i]);
  338. }
  339. } else {
  340. // widthConstraint.maximum NOT defined
  341. for (let i = 0; i < lineCount; i++) {
  342. this.lines.newLine(nlLines[i]);
  343. }
  344. }
  345. }
  346. return this.lines.finalize();
  347. }
  348. /**
  349. * normalize the markup system
  350. *
  351. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  352. * @returns {string}
  353. */
  354. decodeMarkupSystem(markupSystem) {
  355. let system = 'none';
  356. if (markupSystem === 'markdown' || markupSystem === 'md') {
  357. system = 'markdown';
  358. } else if (markupSystem === true || markupSystem === 'html') {
  359. system = 'html'
  360. }
  361. return system;
  362. }
  363. /**
  364. *
  365. * @param {string} text
  366. * @returns {Array}
  367. */
  368. splitHtmlBlocks(text) {
  369. let s = new MarkupAccumulator(text);
  370. let parseEntities = (ch) => {
  371. if (/&/.test(ch)) {
  372. let parsed = s.replace(s.text, '&lt;', '<')
  373. || s.replace(s.text, '&amp;', '&');
  374. if (!parsed) {
  375. s.add("&");
  376. }
  377. return true;
  378. }
  379. return false;
  380. };
  381. while (s.position < s.text.length) {
  382. let ch = s.text.charAt(s.position);
  383. let parsed = s.parseWS(ch)
  384. || (/</.test(ch) && (
  385. s.parseStartTag('bold', '<b>')
  386. || s.parseStartTag('ital', '<i>')
  387. || s.parseStartTag('mono', '<code>')
  388. || s.parseEndTag('bold', '</b>')
  389. || s.parseEndTag('ital', '</i>')
  390. || s.parseEndTag('mono', '</code>')))
  391. || parseEntities(ch);
  392. if (!parsed) {
  393. s.add(ch);
  394. }
  395. s.position++
  396. }
  397. s.emitBlock();
  398. return s.blocks;
  399. }
  400. /**
  401. *
  402. * @param {string} text
  403. * @returns {Array}
  404. */
  405. splitMarkdownBlocks(text) {
  406. let s = new MarkupAccumulator(text);
  407. let beginable = true;
  408. let parseOverride = (ch) => {
  409. if (/\\/.test(ch)) {
  410. if (s.position < this.text.length + 1) {
  411. s.position++;
  412. ch = this.text.charAt(s.position);
  413. if (/ \t/.test(ch)) {
  414. s.spacing = true;
  415. } else {
  416. s.add(ch);
  417. beginable = false;
  418. }
  419. }
  420. return true
  421. }
  422. return false;
  423. }
  424. while (s.position < s.text.length) {
  425. let ch = s.text.charAt(s.position);
  426. let parsed = s.parseWS(ch)
  427. || parseOverride(ch)
  428. || ((beginable || s.spacing) && (
  429. s.parseStartTag('bold', '*')
  430. || s.parseStartTag('ital', '_')
  431. || s.parseStartTag('mono', '`')))
  432. || s.parseEndTag('bold', '*', 'afterBold')
  433. || s.parseEndTag('ital', '_', 'afterItal')
  434. || s.parseEndTag('mono', '`', 'afterMono');
  435. if (!parsed) {
  436. s.add(ch);
  437. beginable = false;
  438. }
  439. s.position++
  440. }
  441. s.emitBlock();
  442. return s.blocks;
  443. }
  444. /**
  445. * Explodes a piece of text into single-font blocks using a given markup
  446. *
  447. * @param {string} text
  448. * @param {boolean|'md'|'markdown'|'html'} markupSystem
  449. * @returns {Array.<{text: string, mod: string}>}
  450. * @private
  451. */
  452. splitBlocks(text, markupSystem) {
  453. let system = this.decodeMarkupSystem(markupSystem);
  454. if (system === 'none') {
  455. return [{
  456. text: text,
  457. mod: 'normal'
  458. }]
  459. } else if (system === 'markdown') {
  460. return this.splitMarkdownBlocks(text);
  461. } else if (system === 'html') {
  462. return this.splitHtmlBlocks(text);
  463. }
  464. }
  465. /**
  466. * @param {string} text
  467. * @returns {boolean} true if text length over the current max with
  468. * @private
  469. */
  470. overMaxWidth(text) {
  471. let width = this.ctx.measureText(text).width;
  472. return (this.lines.curWidth() + width > this.parent.fontOptions.maxWdt);
  473. }
  474. /**
  475. * Determine the longest part of the sentence which still fits in the
  476. * current max width.
  477. *
  478. * @param {Array} words Array of strings signifying a text lines
  479. * @return {number} index of first item in string making string go over max
  480. * @private
  481. */
  482. getLongestFit(words) {
  483. let text = '';
  484. let w = 0;
  485. while (w < words.length) {
  486. let pre = (text === '') ? '' : ' ';
  487. let newText = text + pre + words[w];
  488. if (this.overMaxWidth(newText)) break;
  489. text = newText;
  490. w++;
  491. }
  492. return w;
  493. }
  494. /**
  495. * Determine the longest part of the string which still fits in the
  496. * current max width.
  497. *
  498. * @param {Array} words Array of strings signifying a text lines
  499. * @return {number} index of first item in string making string go over max
  500. */
  501. getLongestFitWord(words) {
  502. let w = 0;
  503. while (w < words.length) {
  504. if (this.overMaxWidth(words.slice(0,w))) break;
  505. w++;
  506. }
  507. return w;
  508. }
  509. /**
  510. * Split the passed text into lines, according to width constraint (if any).
  511. *
  512. * The method assumes that the input string is a single line, i.e. without lines break.
  513. *
  514. * This method retains spaces, if still present (case `font.multi: false`).
  515. * A space which falls on an internal line break, will be replaced by a newline.
  516. * There is no special handling of tabs; these go along with the flow.
  517. *
  518. * @param {string} str
  519. * @param {string} [mod='normal']
  520. * @param {boolean} [appendLast=false]
  521. * @private
  522. */
  523. splitStringIntoLines(str, mod = 'normal', appendLast = false) {
  524. // Still-present spaces are relevant, retain them
  525. str = str.replace(/^( +)/g, '$1\r');
  526. str = str.replace(/([^\r][^ ]*)( +)/g, '$1\r$2\r');
  527. let words = str.split('\r');
  528. while (words.length > 0) {
  529. let w = this.getLongestFit(words);
  530. if (w === 0) {
  531. // Special case: the first word is already larger than the max width.
  532. let word = words[0];
  533. // Break the word to the largest part that fits the line
  534. let x = this.getLongestFitWord(word);
  535. this.lines.newLine(word.slice(0, x), mod);
  536. // Adjust the word, so that the rest will be done next iteration
  537. words[0] = word.slice(x);
  538. } else {
  539. // skip any space that is replaced by a newline
  540. let newW = w;
  541. if (words[w - 1] === ' ') {
  542. w--;
  543. } else if (words[newW] === ' ') {
  544. newW++;
  545. }
  546. let text = words.slice(0, w).join("");
  547. if (w == words.length && appendLast) {
  548. this.lines.append(text, mod);
  549. } else {
  550. this.lines.newLine(text, mod);
  551. }
  552. // Adjust the word, so that the rest will be done next iteration
  553. words = words.slice(newW);
  554. }
  555. }
  556. }
  557. }
  558. export default LabelSplitter;