From bfc7524d1b748bc2d36da703873f5332d3141c63 Mon Sep 17 00:00:00 2001 From: Alfred Melch Date: Fri, 18 Oct 2019 13:51:50 +0900 Subject: [PATCH] Changes --- .gitignore | 1 + components/rsvp-controls.js | 0 components/rsvp-reader.js | 0 components/rsvp-word.js | 51 +++++++++++ index.html | 54 +++++++++--- index.js | 67 +++++++++++++++ spec/Chapter.spec.js | 52 ++++++++++++ spec/breakText.spec.js | 11 --- spec/breakWord.spec.js | 51 ----------- spec/findSentences.spec.js | 23 ----- spec/index.spec.js | 5 +- spec/parseText.spec.js | 47 ++++++++++ src/Chapter.js | 127 ++++++++++++++++++++-------- src/Player.js | 45 ++++++++++ src/textProcessing/breakText.js | 9 -- src/textProcessing/breakWord.js | 31 ------- src/textProcessing/findPivot.js | 15 ---- src/textProcessing/findSentences.js | 14 --- src/textProcessing/parseText.js | 76 +++++++++++++++++ src/textProcessing/pivotize.js | 56 ++++++++++++ 20 files changed, 530 insertions(+), 205 deletions(-) create mode 100644 components/rsvp-controls.js create mode 100644 components/rsvp-reader.js create mode 100644 components/rsvp-word.js create mode 100644 spec/Chapter.spec.js delete mode 100644 spec/breakText.spec.js delete mode 100644 spec/breakWord.spec.js delete mode 100644 spec/findSentences.spec.js create mode 100644 spec/parseText.spec.js create mode 100644 src/Player.js delete mode 100644 src/textProcessing/breakText.js delete mode 100644 src/textProcessing/breakWord.js delete mode 100644 src/textProcessing/findPivot.js delete mode 100644 src/textProcessing/findSentences.js create mode 100644 src/textProcessing/parseText.js create mode 100644 src/textProcessing/pivotize.js diff --git a/.gitignore b/.gitignore index 3c3629e..55371e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.vscode \ No newline at end of file diff --git a/components/rsvp-controls.js b/components/rsvp-controls.js new file mode 100644 index 0000000..e69de29 diff --git a/components/rsvp-reader.js b/components/rsvp-reader.js new file mode 100644 index 0000000..e69de29 diff --git a/components/rsvp-word.js b/components/rsvp-word.js new file mode 100644 index 0000000..8422e60 --- /dev/null +++ b/components/rsvp-word.js @@ -0,0 +1,51 @@ +import { pivotize } from '../src/textProcessing/pivotize.js' + +class RSVPWord extends HTMLElement { + constructor() { + super() + const shadow = this.attachShadow({ mode: 'open' }) + const style = document.createElement('style') + const word = document.createElement('div') + const prefix = document.createElement('span') + const pivot = document.createElement('span') + const suffix = document.createElement('span') + + word.setAttribute('class', 'word') + prefix.setAttribute('class', 'prefix') + pivot.setAttribute('class', 'pivot') + suffix.setAttribute('class', 'suffix') + + style.textContent = + '.word{display:flex}.pivot{color:red}.prefix,.suffix{flex:1}.prefix{text-align:right}' + + word.appendChild(prefix) + word.appendChild(pivot) + word.appendChild(suffix) + shadow.appendChild(style) + shadow.appendChild(word) + + this._root = shadow + this.wordParts = { prefix, pivot, suffix } + } + + connectedCallback() { + this.updateDisplay() + } + + static get observedAttributes() { + return ['word'] + } + + attributeChangedCallback() { + this.updateDisplay() + } + + updateDisplay() { + const [prefix, pivot, suffix] = pivotize(this.getAttribute('word') || '') + this.wordParts.prefix.innerText = prefix + this.wordParts.pivot.innerText = pivot + this.wordParts.suffix.innerText = suffix + } +} + +window.customElements.define('rsvp-word', RSVPWord) diff --git a/index.html b/index.html index cfa20bf..7d5d04e 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,47 @@ - - - - - + + + + Document - + + + + +
+ + +
+ Info: +
+
+
+
|
+ +
|
+
+
+
+ + + + +
+
+ +
+
- -
- - - \ No newline at end of file + + diff --git a/index.js b/index.js index e69de29..9e5578d 100644 --- a/index.js +++ b/index.js @@ -0,0 +1,67 @@ +import { Chapter } from './src/Chapter.js' +import { Player } from './src/Player.js' + +import './components/rsvp-word.js' + +const inputText = document.getElementById('input') +const output = document.getElementById('output') + +const prevSentenceButton = document.getElementById('prevSentence') +const prevWordButton = document.getElementById('prevWord') +const nextWordButton = document.getElementById('nextWord') +const nextSentenceButton = document.getElementById('nextSentence') + +const playButton = document.getElementById('play') + +let chapter = new Chapter(inputText.value, 10) +let player = new Player() + +function updateUI() { + prevSentenceButton.disabled = !chapter.hasPrevSentence() + prevWordButton.disabled = !chapter.hasPrevWord() + nextWordButton.disabled = !chapter.hasNextWord() + nextSentenceButton.disabled = !chapter.hasNextSentence() + + playButton.innerText = player.playing ? 'pause' : 'start' + + output.setAttribute('word', chapter.currentSegment) +} + +function tick() { + if (!chapter.hasNext()) { + player.stop() + } else { + chapter.next() + } + updateUI() +} + +function handleClick(e) { + switch (e.target.getAttribute('action')) { + case 'load': + chapter = new Chapter(inputText.value, 10) + break + case 'prevSentence': + chapter.prevSentence() + break + case 'nextSentence': + chapter.nextSentence() + break + case 'prevWord': + chapter.prevWord() + break + case 'nextWord': + chapter.nextWord() + break + case 'play-pause': + player.toggle() + break + } + updateUI() +} + +for (let button of document.getElementsByClassName('action')) { + button.onclick = handleClick +} +player.subscribe('main', tick) +updateUI() diff --git a/spec/Chapter.spec.js b/spec/Chapter.spec.js new file mode 100644 index 0000000..f082948 --- /dev/null +++ b/spec/Chapter.spec.js @@ -0,0 +1,52 @@ +import { Chapter, _privates } from '../src/Chapter.js' + +const { getNextBiggerNumber } = _privates + +describe('Chapter', function() { + const demoText = + 'Hello World. Foo bar baz. Lorem ipsum dolor sit. Worttrennungsalgorithmus.' + + it('Iterates through segments', function() { + let chapter = new Chapter(demoText, 7) + let i = 1 + while (chapter.next()) i++ + expect(i).toBe(13) + }) + + it('Iterates through words', function() { + let chapter = new Chapter(demoText, 7) + let i = 1 + while (chapter.nextWord()) i++ + expect(i).toBe(10) + }) + + it('Iterates through sentences', function() { + let chapter = new Chapter(demoText) + let i = 1 + while (chapter.nextSentence()) i++ + expect(i).toBe(4) + }) + + it('Iterators return null on finish', function() { + let chapter = new Chapter(demoText, 7) + let cur + while ((cur = chapter.next())) {} + expect(cur).toBe(null) + while ((cur = chapter.prev())) {} + expect(cur).toBe(null) + while ((cur = chapter.nextWord())) {} + expect(cur).toBe(null) + while ((cur = chapter.prevWord())) {} + expect(cur).toBe(null) + while ((cur = chapter.nextSentence())) {} + expect(cur).toBe(null) + while ((cur = chapter.prevSentence())) {} + expect(cur).toBe(null) + }) +}) + +describe('nextBiggerNumber', function() { + it('Returns a the next bigger number', function() { + expect(getNextBiggerNumber(5, [1, 4, 6])).toBe(6) + }) +}) diff --git a/spec/breakText.spec.js b/spec/breakText.spec.js deleted file mode 100644 index b1c5829..0000000 --- a/spec/breakText.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import { breakText } from '../src/textProcessing/breakText.js' - -describe('breakText', function() { - it('returns an array', function() { - expect(Array.isArray(breakText('Hello World'))).toBeTruthy() - }) - - it('array is of appropriate length', function() { - expect(breakText('Hello World').length).toBe(2) - }) -}) diff --git a/spec/breakWord.spec.js b/spec/breakWord.spec.js deleted file mode 100644 index e1e853a..0000000 --- a/spec/breakWord.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { - breakWordSegment, - splitLongWord, - breakWord -} from '../src/textProcessing/breakWord.js' - -describe('breakWordSegment', function() { - it('returns an array', function() { - expect(Array.isArray(breakWordSegment('asdf'))).toBeTruthy() - }) - it('returns triples', function() { - expect(breakWordSegment('asdf').length).toBe(3) - }) -}) - -describe('splitLongWord', function() { - it('returns an array', function() { - expect(Array.isArray(splitLongWord('asdf'))).toBeTruthy() - }) - - it('returns the single word by default', function() { - let segments = splitLongWord('asdf') - expect(segments.length).toBe(1) - expect(segments[0]).toBe('asdf') - }) - - it('returns small words unmodified', function() { - let segments = splitLongWord('asdf') - expect(segments.length).toBe(1) - expect(segments[0]).toBe('asdf') - }) - - it('splits long words', function() { - let segments = splitLongWord('asdf', 3) - expect(segments.length).toBe(2) - expect(segments[0]).toBe('asd') - expect(segments[1]).toBe('f') - }) - - it('split into even parts', function() { - let segments = splitLongWord('1234567890', 9) - expect(segments[0].length).toBe(5) - expect(segments[1].length).toBe(5) - }) -}) - -describe('breakWord', function() { - it('returns an array', function() { - expect(Array.isArray(breakWord('asdf'))).toBeTruthy() - }) -}) diff --git a/spec/findSentences.spec.js b/spec/findSentences.spec.js deleted file mode 100644 index eed407d..0000000 --- a/spec/findSentences.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import { - findSentences -} from '../src/textProcessing/findSentences.js' - -describe('findSentences', function () { - it('returns an array', function () { - expect(Array.isArray(findSentences(['Hello', 'World']))).toBeTruthy() - }) - - it('finds a single sentence', function () { - let sentences = findSentences(['Hello'], ['World']) - expect(sentences.length).toBe(1) - expect(sentences[0]).toBe(0) - }) - - it('finds two sentences', function () { - let sentences = findSentences(['Hello', 'World.', 'Foo', 'bar.']) - expect(sentences.length).toBe(2) - expect(sentences[0]).toBe(0) - expect(sentences[1]).toBe(2) - }) - -}) \ No newline at end of file diff --git a/spec/index.spec.js b/spec/index.spec.js index e9063ba..6b91b84 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -1,4 +1,3 @@ -import './breakText.spec.js' -import './breakWord.spec.js' +import './Chapter.spec.js' import './findPivot.spec.js' -import './findSentences.spec.js' \ No newline at end of file +import './parseText.spec.js' diff --git a/spec/parseText.spec.js b/spec/parseText.spec.js new file mode 100644 index 0000000..fc68ec7 --- /dev/null +++ b/spec/parseText.spec.js @@ -0,0 +1,47 @@ +import { parseText, _privates } from '../src/textProcessing/parseText.js' + +const { splitLongWord } = _privates + +describe('parseText', function() { + it('returns an object with expected properties', function() { + let parsed = parseText('Hello World. Test Sentence.') + expect(parsed.segments).toEqual(['Hello', 'World.', 'Test', 'Sentence.']) + expect(parsed.words).toEqual([0, 1, 2, 3]) + expect(parsed.sentences).toEqual([0, 2]) + }) +}) + +describe('splitLongWord', function() { + it('returns an array', function() { + expect(Array.isArray(splitLongWord('asdf'))).toBeTruthy() + }) + + it('returns the single word by default', function() { + expect(splitLongWord('asdf')).toEqual(['asdf']) + }) + + it('returns small words unmodified', function() { + expect(splitLongWord('asdf', 5)).toEqual(['asdf']) + expect(splitLongWord('asdf', 4)).toEqual(['asdf']) + }) + + it('splits long words', function() { + expect(splitLongWord('asdf', 3)).toEqual(['as', 'df']) + }) + + it('split into even parts', function() { + expect(splitLongWord('1234567890', 9)).toEqual(['12345', '67890']) + }) + + it('corner case: uneven length', function() { + expect(splitLongWord('123456789', 8)).toEqual(['1234', '56789']) + }) + + it('corner case: multiple uneven parts', function() { + let word = '1234567890123' + let segments = splitLongWord(word, 3) + expect(segments.reduce((x, y) => x + y, '')).toBe(word) + expect(Math.max(...segments.map(seg => seg.length))).toBe(3) + expect(Math.min(...segments.map(seg => seg.length))) + }) +}) diff --git a/src/Chapter.js b/src/Chapter.js index 37a7758..cef71e9 100644 --- a/src/Chapter.js +++ b/src/Chapter.js @@ -1,66 +1,119 @@ -import { - findSentences -} from "./textProcessing/findSentences"; +import { parseText } from './textProcessing/parseText.js' -class Chapter { - constructor(text = '') { - this.setText(text) +export class Chapter { + constructor(text, maxLength = -1) { + let { segments, words, sentences } = parseText(text, maxLength) + this.segments = segments + this.words = words + this.sentences = sentences + this.currentIdx = 0 } - setText(text) { - this.words = breakText(text) - this.first = 0 - this.last = words.length - 1 - this.current = 0 - this.sentences = findSentences(this.words) + get currentSegment() { + return this.segments[this.currentIdx] } - get curWord() { - return this.words[this.current] + get metainfo() { + return { + segmentCount: this.segments.length, + wordsCount: this.words.length, + sentenceCount: this.sentences.length, + currentSegment: currentIdx + 1, + currentWord: -1, + currentSentence: -1 + } } next() { - this.current = this.current + 1 - return this.curWord + if (!this.hasNext()) return null + this.currentIdx += 1 + return this.currentSegment } prev() { - this.current = this.curren - 1 - return this.curWord + if (!this.hasPrev()) return null + this.currentIdx -= 1 + return this.currentSegment } - hasNext() { - return this.current < this.last + nextWord() { + if (!this.hasNextWord()) return null + this.currentIdx = getNextBiggerNumber(this.currentIdx, this.words) + return this.currentSegment } - hasPrev() { - this.current > this.first + prevWord() { + if (!this.hasPrevWord()) return null + this.currentIdx = getNextSmallerNumber(this.currentIdx, this.words) + return this.currentSegment } nextSentence() { - for (let sentence of this.sentences) { - if (sentence > this.current) { - this.current = sentence - return this.curWord - } - } + if (!this.hasNextSentence()) return null + this.currentIdx = getNextBiggerNumber(this.currentIdx, this.sentences) + return this.currentSegment } prevSentence() { - for (let sentence of this.sentences.reverse()) { - if (sentence < this.current) { - this.current = sentence - return this.curWord - } - } + if (!this.hasPrevSentence()) return null + this.currentIdx = getNextSmallerNumber(this.currentIdx, this.sentences) + return this.currentSegment + } + + hasNext() { + return this.currentIdx < this.segments.length - 1 + } + + hasPrev() { + return this.currentIdx > 0 + } + + hasNextWord() { + return this.currentIdx < lastEntry(this.words) + } + + hasPrevWord() { + return this.currentIdx > this.words[0] } hasNextSentence() { - return this.current < this.sentences.reverse[0] + return this.currentIdx < lastEntry(this.sentences) } hasPrevSentence() { - + return this.currentIdx > this.sentences[0] } +} -} \ No newline at end of file +/** + * Returns the next bigger number from a sorted Array of numbers. + * Returns null if num is the biggest number + * @param {Number} idx + * @param {Array} sortedArray + */ +function getNextBiggerNumber(num, sortedArray) { + for (let currentNumber of sortedArray) { + if (currentNumber > num) return currentNumber + } + return null +} + +/** + * Returns the next smaller number from a sorted Array of numbers. + * Returns null if num is the smallest number + * @param {Number} idx + * @param {Array} sortedArray + */ +function getNextSmallerNumber(num, sortedArray) { + let reversedArray = [...sortedArray].reverse() + for (let currentNumber of reversedArray) { + if (currentNumber < num) return currentNumber + } + return null +} + +function lastEntry(arr) { + return arr[arr.length - 1] +} + +export const _privates = { getNextBiggerNumber } diff --git a/src/Player.js b/src/Player.js new file mode 100644 index 0000000..b8aa90d --- /dev/null +++ b/src/Player.js @@ -0,0 +1,45 @@ +export class Player { + constructor(interval = 100) { + this.intervalHandle = null + this.interval = interval + this.subscribers = {} + } + + get playing() { + return this.intervalHandle !== null + } + + start() { + clearInterval(this.intervalHandle) + this.intervalHandle = setInterval(this.tick.bind(this), this.interval) + } + + stop() { + clearInterval(this.intervalHandle) + this.intervalHandle = null + } + + toggle() { + if (this.playing) this.stop() + else this.start() + } + + updateInterval(interval) { + this.interval = interval + if (this.intervalHandle) this.start() + } + + tick() { + for (let callback of Object.values(this.subscribers)) { + callback() + } + } + + subscribe(name, callback) { + this.subscribers[name] = callback + } + + unsubscribe(name) { + delete subscribers[name] + } +} diff --git a/src/textProcessing/breakText.js b/src/textProcessing/breakText.js deleted file mode 100644 index 5409e4f..0000000 --- a/src/textProcessing/breakText.js +++ /dev/null @@ -1,9 +0,0 @@ -export function breakText(text) { - let words = [] - for (let word of text.trim().split(/[ \t\n]/)) { - if (word.trim() !== '') { - words.push(word) - } - } - return words -} \ No newline at end of file diff --git a/src/textProcessing/breakWord.js b/src/textProcessing/breakWord.js deleted file mode 100644 index 74dda07..0000000 --- a/src/textProcessing/breakWord.js +++ /dev/null @@ -1,31 +0,0 @@ -import { findPivot } from './findPivot.js' - -export function breakWordSegment(word, mode) { - let start = 0 - let end = word.length - let pivot = findPivot(word) - return [ - word.slice(start, pivot), - word.slice(pivot, pivot + 1), - word.slice(pivot + 1, end) - ] -} - -export function splitLongWord(word, maxLength) { - if (maxLength === -1) return [word] - let segments = [] - let segmentStart = 0 - let cur = word.slice(segmentStart, maxLength) - while (cur !== '') { - segments.push(cur) - segmentStart += maxLength - cur = word.slice(segmentStart, segmentStart + maxLength) - } - return segments -} - -export function breakWord(word, mode, maxLength = -1) { - return splitLongWord(word, maxLength).map(word => - breakWordSegment(word, mode) - ) -} diff --git a/src/textProcessing/findPivot.js b/src/textProcessing/findPivot.js deleted file mode 100644 index 5499dd5..0000000 --- a/src/textProcessing/findPivot.js +++ /dev/null @@ -1,15 +0,0 @@ -export function findPivot(word) { - const table = { - 1: 0, - 2: 0, - 3: 1, - 4: 1, - 5: 2, - 6: 2, - 7: 2, - 8: 3, - 9: 3 - } - return typeof table[word.length] === 'undefined' ? 4 : table[word.length] - -} \ No newline at end of file diff --git a/src/textProcessing/findSentences.js b/src/textProcessing/findSentences.js deleted file mode 100644 index 3eed0f9..0000000 --- a/src/textProcessing/findSentences.js +++ /dev/null @@ -1,14 +0,0 @@ -export function findSentences(words) { - let sentences = [] - let sentenceFlag = true - for (let [idx, word] of words.entries()) { - if (sentenceFlag) { - sentences.push(idx) - sentenceFlag = false - } - if (word.endsWith('.')) { - sentenceFlag = true - } - } - return sentences -} \ No newline at end of file diff --git a/src/textProcessing/parseText.js b/src/textProcessing/parseText.js new file mode 100644 index 0000000..7c36c95 --- /dev/null +++ b/src/textProcessing/parseText.js @@ -0,0 +1,76 @@ +/** + * Returns an object containing the segmented text and metainfo about word and + * sentence beginnings + * @param {String} text + * @param {Number} maxLength + * @returns {Object} + */ +export function parseText(text, maxLength) { + let segments = [] + let words = [] + let sentences = [] + let curIdx = 0 + let sentenceFlag = true + + for (let word of extractWords(text)) { + // fill metainfo + words.push(curIdx) + if (sentenceFlag) { + sentences.push(curIdx) + } + // fragmentize word if necessary and fill segments + let fragments = splitLongWord(word, maxLength) + segments.push(...fragments) + curIdx += fragments.length + + // set flag if next word is sentence beginning + sentenceFlag = word.endsWith('.') + } + + return { segments, words, sentences } +} + +/** + * Returns an Array words from a text. Words are identified by whitespace. + * @param {String} text + * @returns {Array} + */ +function extractWords(text) { + let words = [] + for (let word of text.trim().split(/[ \t\n]/)) { + if (word.trim() !== '') { + words.push(word) + } + } + return words +} + +/** + * Splits a word evenly in parts with maximum length of maxLength. + * Todo: more intelligent hyphenation algorithm + * @param {String} word + * @param {Number} maxLength + * @returns {Array} word fragents + */ +function splitLongWord(word, maxLength = -1) { + if (maxLength === -1) return [word] + if (maxLength === word.length) return [word] + + let fragments = [] + let numParts = Math.floor(word.length / maxLength) + 1 + let step = word.length / numParts + let start = 0 + let end = step + + while (start < word.length) { + fragments.push(word.slice(start, end)) + start += step + end += step + } + for (let i = 0; i < fragments.length - 1; i++) { + fragments[i] = fragments[i] + '-' + } + return fragments +} + +export const _privates = { extractWords, splitLongWord } diff --git a/src/textProcessing/pivotize.js b/src/textProcessing/pivotize.js new file mode 100644 index 0000000..ac40237 --- /dev/null +++ b/src/textProcessing/pivotize.js @@ -0,0 +1,56 @@ +function calculatePivot(word) { + const splits = [ + 0, + 0, + 0, //012 + 1, + 1, + 1, //345 + 2, + 2, + 2, + 2, //6789 + 3, + 3, + 3, + 3, + 3, + 3, //10-15 + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4 + ] + return typeof splits[word.length] === 'undefined' ? 5 : splits[word.length] +} + +export function pivotIdx(word) { + const table = { + 1: 0, + 2: 0, + 3: 1, + 4: 1, + 5: 2, + 6: 2, + 7: 2, + 8: 3, + 9: 3, + 10: 3 + } + return typeof table[word.length] === 'undefined' ? 4 : table[word.length] +} + +export function pivotize(word, mode) { + let start = 0 + let end = word.length + let pivot = calculatePivot(word) + return [ + word.slice(start, pivot), + word.slice(pivot, pivot + 1), + word.slice(pivot + 1, end) + ] +}