├── .gitignore ├── assets └── spencer-image.png ├── tsconfig.json ├── .github └── workflows │ └── npm-publish.yml ├── package.json ├── lib ├── index.css ├── index.d.ts ├── index.js.map └── index.js ├── local-test.html ├── demo └── index.html ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /assets/spencer-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackyzha0/telescopic-text/HEAD/assets/spencer-image.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "sourceMap": true, 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "target": "es5", 8 | "lib": [ 9 | "es2019", 10 | "dom" 11 | ] 12 | }, 13 | "include": ["src/"], 14 | "exclude": ["node_modules", "**/__tests__/*"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 16 14 | - run: npm install 15 | - id: publish 16 | uses: JS-DevTools/npm-publish@v1 17 | with: 18 | token: ${{ secrets.NPM_AUTH_TOKEN }} 19 | check-version: true 20 | - if: steps.publish.outputs.type != 'none' 21 | run: | 22 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telescopic-text", 3 | "version": "1.2.6", 4 | "description": "A open-source library to help with creating expandable text", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "files": [ 11 | "lib" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jackyzha0/telescopic-text.git" 16 | }, 17 | "author": "Jacky Zhao", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/jackyzha0/telescopic-text/issues" 21 | }, 22 | "homepage": "https://github.com/jackyzha0/telescopic-text#readme", 23 | "devDependencies": { 24 | "@types/marked": "^4.0.2", 25 | "@types/node": "^17.0.17", 26 | "typescript": "^4.5.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --telescope-highlight: #fcc16d55; 3 | --telescope-highlight-hover: #fcc16d; 4 | --telescope-text-color: #1f2021; 5 | } 6 | 7 | #telescope { 8 | color: var(--telescope-text-color); 9 | } 10 | 11 | #telescope#telescope blockquote { 12 | margin-left: 0; 13 | padding-left: 1rem; 14 | border-left: 3px solid var(--telescope-text-color); 15 | display: block; 16 | } 17 | 18 | #telescope#telescope hr { 19 | border: 1px solid var(--telescope-text-color); 20 | margin: 1em 0; 21 | display: block; 22 | } 23 | 24 | #telescope .details { 25 | display: inline; 26 | border-radius: 3px; 27 | background-color: var(--telescope-highlight); 28 | transition: background-color 0.5s ease; 29 | cursor: pointer; 30 | } 31 | 32 | #telescope .details.open { 33 | background-color: transparent; 34 | cursor: initial; 35 | } 36 | 37 | #telescope .details.open > .summary, #telescope .details.close > .expanded { 38 | display: none; 39 | } 40 | 41 | #telescope .details.close:hover { 42 | background-color: var(--telescope-highlight-hover); 43 | } 44 | 45 | #telescope .details *, #telescope p * { 46 | list-style: none; 47 | display: inline; 48 | } 49 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Line { 2 | og: string; 3 | new: string; 4 | replacements: Line[]; 5 | } 6 | interface Content { 7 | text: string; 8 | replacements: Line[]; 9 | } 10 | interface NewContent { 11 | text: string; 12 | expansions?: NewContent[]; 13 | separator?: string; 14 | } 15 | declare type TelescopicOutput = NewContent[]; 16 | interface TelescopeNode { 17 | depth: number; 18 | telescopicOut: TelescopicOutput; 19 | } 20 | /** 21 | * Modes that designate what form the input text is in and should be interpreted as. 22 | */ 23 | declare enum TextMode { 24 | Text = "text", 25 | /** 26 | * NOTE: this uses `innerHtml` under the hood, so should not be used with untrusted user input and can have 27 | * unexpected behavior if the HTML is not valid. 28 | */ 29 | Html = "html" 30 | } 31 | declare function isTextMode(e: any): e is TextMode[keyof TextMode]; 32 | interface Config { 33 | /** 34 | * Character used to separate entries on the same level. Defaults to a single space (" ") 35 | */ 36 | separator?: string; 37 | /** 38 | * If true, allows sections to expand automatically on mouse over rather than requiring a click. Defaults to false. 39 | */ 40 | shouldExpandOnMouseOver?: boolean; 41 | /** 42 | * A mode that designates what form the input text is in and should be interpreted as. Defaults to 'text'. 43 | */ 44 | textMode?: TextMode; 45 | /** 46 | * Determines the wrapper element type for HTML elements. Defaults to 'span'. 47 | */ 48 | htmlContainerTag?: keyof HTMLElementTagNameMap; 49 | /** 50 | * Only valid when textMode is 'text'. Used to insert HTML element like blockquotes, line breaks, bold, and emphasis in plain text mode. 51 | */ 52 | specialCharacters?: TextReplacements; 53 | } 54 | declare type CreateTelescopicTextConfig = Pick; 55 | declare let _lastHoveredTime: number; 56 | interface TextReplacements { 57 | [key: string]: (lineText: string) => HTMLElement; 58 | } 59 | declare const DefaultReplacements: TextReplacements; 60 | declare function _hydrate(line: Content, node: any, config?: CreateTelescopicTextConfig): void; 61 | declare function _createTelescopicText(content: Content[], config?: CreateTelescopicTextConfig): HTMLDivElement; 62 | /*****************/ 63 | /*****************/ 64 | declare function _parseMarkdown(mdContent: string): TelescopicOutput; 65 | declare function _parseOutputIntoContent(output: TelescopicOutput, separator?: string): Content; 66 | declare function _parseMarkdownIntoContent(mdContent: string, separator?: string): Content; 67 | /** 68 | * Parses a markdown-compatible bulleted list into an HTML div that contains the telescoping text specified by the bullet list content. 69 | * 70 | * @param listContent - Content in the form of a bulleted list where items on the same level are combined using the `separator` parameter. 71 | * @param config - Configuration options provided to create interactive, telescopic text. 72 | * @returns HTML div containing the telescoping text. 73 | */ 74 | declare function createTelescopicTextFromBulletedList(listContent: string, { separator, ...createTelescopicTextConfig }?: Config): HTMLDivElement; 75 | -------------------------------------------------------------------------------- /local-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | boundless shapeshifters 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 | 77 | 78 | 109 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | boundless shapeshifters 8 | 9 | 10 | 14 | 15 | 16 |
17 |
18 | 54 Views by Tsherin Sherpa 19 |
54 Views by Tsherin Sherpa
20 |
21 |

boundless shapeshifters

22 |
23 |

- spencer chang

24 |
25 | 26 | 59 | 60 | 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔭 Telescopic Text 2 | 3 | [![](https://img.shields.io/npm/v/telescopic-text?style=flat-square)](https://www.npmjs.com/package/telescopic-text) 4 | 5 | An open-source library to help with creating expandable text. 6 | 7 | Inspired by [StretchText](https://en.wikipedia.org/wiki/StretchText) and [TelescopicText](https://www.telescopictext.org/text/KPx0nlXlKTciC). 8 | 9 | # Background 10 | 11 | I've been thinking a lot about creating a browsable store of knowledge that provides something useful at all distance scales 12 | (e.g. viewing the entire library, just a subcategory, a single file, etc.) and concepts like Telescopic Text are a first step 13 | in creating more information scales than just a single document level. 14 | 15 | This library is meant to be the start for anyone to create telescopic text, including those who are non-technical. 16 | 17 | ## Creating a telescopic text 18 | 19 | To create some telescopic text, you can use your favorite note-taking app or text editor that supports bullet lists and start by writing a full sentence or two in a starting bullet: 20 | 21 | Head to https://poems.verses.xyz/test to use an interactive playground for writing in bullet lists and get the corresponding code that leverages this library to generate the interactive text for use on your own website. 22 | 23 | _NOTE_: the parsing logic is robust to different indentation levels, but for most compatible experience, you should normalize the indentations so that each nested bullet is differentiated by a standard set of spaces. We also currently only support `*`, `-`, and `+` bullet indicators. 24 | 25 | ## Usage 26 | 27 | Create some expandable text using the bullet list format shown above. You can then test out how your poem looks and feels and get a basic code snippet that recreates it using the [test bed](https://poems.verses.xyz/test)! 28 | 29 | See the full instructions below: 30 | 31 | You can load it directly via CDN as follows: 32 | Put this anywhere inside the `head` of your HTML file. 33 | 34 | ```html 35 | 36 | ... 37 | 38 | 42 | 43 | ``` 44 | 45 | or if you prefer to do the manual way, you can include the `lib/index.js` and `lib/index.css` files in your project. 46 | 47 | The package exports a function called `createTelescopicTextFromBulletedList` that parses a bulleted list and returns a HTMLNode with your telescopic text inside. 48 | 49 | Basic usage may look something like this: 50 | 51 | ```html 52 | 53 | 54 | 58 | 59 | 60 |
61 | 62 | 71 | 72 | ``` 73 | 74 | ### Advanced Usage Options 75 | 76 | For further, customization, we provide a configuration object that can be passed into the function for different behavior. 77 | 78 | ```typescript 79 | // The configuration for how you want telescopic text to be parsed and rendered 80 | interface Config { 81 | /** 82 | * Character used to separate entries on the same level. Defaults to a single space (" ") 83 | */ 84 | separator?: string; 85 | /** 86 | * If true, allows sections to expand automatically on mouse over rather than requiring a click. Defaults to false. 87 | */ 88 | shouldExpandOnMouseOver?: boolean; 89 | /** 90 | * A mode that designates what form the input text is in and should be interpreted as. Defaults to 'text'. 91 | */ 92 | textMode?: TextMode; 93 | /** 94 | * Determines the wrapper element type for HTML elements. Defaults to 'span'. 95 | */ 96 | htmlContainerTag?: keyof HTMLElementTagNameMap; 97 | /** 98 | * Only valid when textMode is 'text'. Used to insert HTML element like blockquotes, line breaks, bold, and emphasis in plain text mode. 99 | */ 100 | specialCharacters?: TextReplacements; 101 | } 102 | ``` 103 | 104 | You would use this by passing a custom configuration object into the function in order to override any of the defaults above. For example, this is how you would create telescopic text with custom HTML tags: 105 | 106 | ```javascript 107 | const content = ` 108 | * Some rich text 109 | * with nested rich text 110 | `; 111 | const config = { textMode: TextMode.Html }; 112 | const poemContent = createTelescopicTextFromBulletedList(content, config); 113 | ``` 114 | 115 | You can check out a more detailed example in `demo/index.html` 116 | 117 | If you are using plain 'text' as the textMode, you can also define an object containing special characters and the rules for how to replace them. 118 | 119 | ```typescript 120 | interface TextReplacements { 121 | // Each entry is keyed by its regex string match 122 | // It defines a function that takes in the current line of text as well as its parent node 123 | // and 124 | [key: string]: (lineText: string) => HTMLElement 125 | } 126 | 127 | // for example, here's a text replacement rule for bolding text that is wrapped with * 128 | "\\*(.*)\\*": (lineText) => { 129 | const el = document.createElement("strong"); 130 | el.appendChild(document.createTextNode(lineText)); 131 | return el; 132 | } 133 | ``` 134 | 135 | By default, only these special characters have text replacements 136 | 137 | - line breaks (`---`) 138 | - bold (`*...*`) 139 | - emphasis (`_..._`) 140 | To disable this, you can pass in an empty object for special characters: 141 | 142 | ```typescript 143 | const poemContent = createTelescopicTextFromBulletedList(content, { 144 | specialCharacters: {}, 145 | }); 146 | ``` 147 | 148 | ## Types 149 | 150 | ```typescript 151 | // Default function to create a new `
` node containing the 152 | // telescoping text from bullet points 153 | function createTelescopicTextFromBulletedList(content: string, config?: Config); 154 | ``` 155 | 156 | ## Future Work 157 | 158 | See our issues page for all the features we're thinking about exploring. Some examples include: 159 | 160 | - Supporting infinite expansion with `...` 161 | - Concept of shapeshifting text in general... these are not always expansions. 162 | 163 | ## Development 164 | 165 | - NOTE: avoid the usage of `export` keyword. it breaks in browser and we haven't figured out how to support it cross-function. 166 | -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | interface Line { 2 | og: string; // the original string to replace 3 | new: string; // the replacement string 4 | replacements: Line[]; // nested replacements to apply on the resultant line afterwards 5 | } 6 | 7 | interface Content { 8 | text: string; // Original string content in the line 9 | replacements: Line[]; // Sections of the original text to replace/expand 10 | } 11 | 12 | interface NewContent { 13 | text: string; 14 | expansions?: NewContent[]; 15 | separator?: string; 16 | } 17 | 18 | type TelescopicOutput = NewContent[]; 19 | 20 | interface TelescopeNode { 21 | depth: number; 22 | telescopicOut: TelescopicOutput; 23 | } 24 | 25 | /** 26 | * Modes that designate what form the input text is in and should be interpreted as. 27 | */ 28 | enum TextMode { 29 | Text = "text", 30 | /** 31 | * NOTE: this uses `innerHtml` under the hood, so should not be used with untrusted user input and can have 32 | * unexpected behavior if the HTML is not valid. 33 | */ 34 | Html = "html", 35 | } 36 | // internal fn to typeguard TextMode 37 | function isTextMode(e: any): e is TextMode[keyof TextMode] { 38 | return Object.values(TextMode).includes(e); 39 | } 40 | 41 | interface Config { 42 | /** 43 | * Character used to separate entries on the same level. Defaults to a single space (" ") 44 | */ 45 | separator?: string; 46 | /** 47 | * If true, allows sections to expand automatically on mouse over rather than requiring a click. Defaults to false. 48 | */ 49 | shouldExpandOnMouseOver?: boolean; 50 | /** 51 | * A mode that designates what form the input text is in and should be interpreted as. Defaults to 'text'. 52 | */ 53 | textMode?: TextMode; 54 | /** 55 | * Determines the wrapper element type for HTML elements. Defaults to 'span'. 56 | */ 57 | htmlContainerTag?: keyof HTMLElementTagNameMap; 58 | /** 59 | * Only valid when textMode is 'text'. Used to insert HTML element like blockquotes, line breaks, bold, and emphasis in plain text mode. 60 | */ 61 | specialCharacters?: TextReplacements; 62 | } 63 | 64 | type CreateTelescopicTextConfig = Pick< 65 | Config, 66 | "shouldExpandOnMouseOver" | "textMode" | "htmlContainerTag" | "specialCharacters" 67 | >; 68 | 69 | // time; recorded to prevent recursive text expansion on single hover 70 | let _lastHoveredTime = Date.now(); 71 | 72 | interface TextReplacements { 73 | // Each entry is keyed by its regex string match 74 | // It defines a function that takes in the current line of text as well as its parent node 75 | // and 76 | [key: string]: (lineText: string) => HTMLElement 77 | } 78 | 79 | // By default, only these special characters have text replacements 80 | // - line breaks 81 | // - bold 82 | // - emphasis 83 | const DefaultReplacements: TextReplacements = { 84 | // line break 85 | "---": () => { 86 | return document.createElement("hr"); 87 | }, 88 | // bold 89 | "\\*(.*)\\*": (lineText) => { 90 | const el = document.createElement("strong"); 91 | el.appendChild(document.createTextNode(lineText)); 92 | return el; 93 | }, 94 | // emphasis 95 | "_(.*)_": (lineText) => { 96 | const el = document.createElement("em"); 97 | el.appendChild(document.createTextNode(lineText)); 98 | return el; 99 | } 100 | } 101 | 102 | // Internal recursive function to hydrate a node with a line object. 103 | function _hydrate( 104 | line: Content, 105 | node: any, 106 | config: CreateTelescopicTextConfig = {} 107 | ) { 108 | const { 109 | shouldExpandOnMouseOver, 110 | textMode = TextMode.Text, 111 | htmlContainerTag = "span", 112 | specialCharacters = DefaultReplacements, 113 | } = config; 114 | 115 | let lineText = line.text; 116 | 117 | function createLineNode(lineText: string) { 118 | switch (textMode) { 119 | case TextMode.Text: 120 | for (const [specialCharRegex, replacementFn] of Object.entries(specialCharacters)) { 121 | const matches = lineText.match(specialCharRegex) 122 | if (matches) { 123 | const container = document.createElement(htmlContainerTag); 124 | const [wholeMatch, innerMatchText, _] = matches 125 | // compute pre and post match text 126 | const [pre, post] = lineText.split(wholeMatch) 127 | container.appendChild(createLineNode(pre)) 128 | container.appendChild(replacementFn(innerMatchText)) 129 | container.appendChild(createLineNode(post)) 130 | return container 131 | } 132 | } 133 | // leaf, no special characters 134 | return document.createTextNode(lineText); 135 | 136 | case TextMode.Html: 137 | const newNode = document.createElement(htmlContainerTag); 138 | newNode.innerHTML = lineText; 139 | return newNode; 140 | 141 | default: 142 | throw new Error("Invalid text mode found: " + textMode); 143 | } 144 | } 145 | 146 | // only iterate lines if there are actually things to replace 147 | for (let i = 0; i < line.replacements.length; i++) { 148 | const replacement = line.replacements[i]; 149 | 150 | // split single occurrence of replacement pattern 151 | const [before, ...after] = lineText.split(replacement.og); 152 | lineText = after.join(replacement.og); 153 | 154 | // add old real text 155 | node.appendChild(createLineNode(before)); 156 | 157 | // create actual telescope 158 | const detail = document.createElement("span"); 159 | detail.classList.add("details", "close"); 160 | 161 | // add expand on click handler 162 | detail.addEventListener("click", () => { 163 | detail.classList.remove("close"); 164 | detail.classList.add("open"); 165 | }); 166 | 167 | if (shouldExpandOnMouseOver) { 168 | // expand the text if text was not moused over immediately before 169 | detail.addEventListener("mouseover", () => { 170 | if (Date.now() - _lastHoveredTime > 10) { 171 | detail.classList.remove("close"); 172 | detail.classList.add("open"); 173 | _lastHoveredTime = Date.now(); 174 | } 175 | }); 176 | } 177 | 178 | const summary = document.createElement("span"); 179 | summary.classList.add("summary"); 180 | const newNode = createLineNode(replacement.og); 181 | summary.appendChild(newNode); 182 | detail.appendChild(summary); 183 | 184 | // create inner text, recursively hydrate 185 | const newLine = { 186 | text: replacement.new, 187 | replacements: replacement.replacements, 188 | }; 189 | const expanded = document.createElement("span"); 190 | expanded.classList.add("expanded"); 191 | _hydrate(newLine, expanded, config); 192 | 193 | // append to parent 194 | detail.appendChild(expanded); 195 | node.appendChild(detail); 196 | } 197 | if (lineText) { 198 | // otherwise, this is a leaf node 199 | const newNode = createLineNode(lineText); 200 | node.appendChild(newNode); 201 | } 202 | } 203 | 204 | // Helper function to create a new `
` node containing the 205 | // telescoping text. 206 | function _createTelescopicText( 207 | content: Content[], 208 | config: CreateTelescopicTextConfig = {} 209 | ) { 210 | const letter = document.createElement("div"); 211 | letter.id = "telescope"; 212 | content.forEach((line) => { 213 | const newNode = document.createElement("p"); 214 | _hydrate(line, newNode, config); 215 | letter.appendChild(newNode); 216 | }); 217 | return letter; 218 | } 219 | 220 | /*****************/ 221 | /* PARSING LOGIC */ 222 | /*****************/ 223 | 224 | // Parses the input string and returns the output as a structured data format. 225 | function _parseMarkdown(mdContent: string): TelescopicOutput { 226 | // In future we might want to support full markdown in which case.. 227 | // const html = marked.parse(mdContent); 228 | // convert into jsdom 229 | // const lines = html.split("\n"); 230 | // Also idea for "..." or ellipsis character to repreesnt infinite expansion. 231 | 232 | const BulletSeparators = ["*", "-", "+"]; 233 | const RegexEscapedBulletSeparators = ["\\*", "-", "\\+"]; 234 | 235 | const lines = mdContent.split("\n"); 236 | // NOTE: this should handle normalizing the depth (if its an indented list) 237 | const root: TelescopicOutput = []; 238 | const nodeStack: TelescopeNode[] = [{ depth: 0, telescopicOut: root }]; 239 | 240 | // This is essentially a trie data structure to parse out all the bullet points 241 | // The algorithm works by assuming that any time you encounter a longer depth than the current one, 242 | // you are moving onto the next line. 243 | const firstNonEmptyLine = lines.find((l) => l.trim().length > 0); 244 | const defaultDepth = 245 | firstNonEmptyLine?.match( 246 | `^\\s*(${RegexEscapedBulletSeparators.join("|")})` 247 | )?.[0]?.length - 1 || 0; 248 | for (const line of lines) { 249 | const trimmedLine = line.trim(); 250 | if (!trimmedLine.length) { 251 | continue; 252 | } 253 | 254 | // validate that the trimmed line starts with the bullet indicators. 255 | if (!BulletSeparators.some((sep) => trimmedLine.startsWith(sep))) { 256 | console.log(`Invalid line found! ${line}`); 257 | continue; 258 | } 259 | 260 | // count all spaces in front to get current depth 261 | const currentDepth = 262 | line.match(`^\\s*(${RegexEscapedBulletSeparators.join("|")})`)![0] 263 | .length - 1; 264 | 265 | // if greater depth, attach on to the last one 266 | // else you can pop back up to one with 1 less depth 267 | while ( 268 | nodeStack.length - 1 >= 0 && 269 | currentDepth <= nodeStack[nodeStack.length - 1].depth && 270 | nodeStack[nodeStack.length - 1].depth > 0 271 | ) { 272 | nodeStack.pop(); 273 | } 274 | 275 | const { telescopicOut, ...restLastNode } = nodeStack[nodeStack.length - 1]; 276 | const strippedLine = trimmedLine.substring(1).replace(/^\s+/, ""); 277 | // Add current content / node to the stack 278 | const currentContent: NewContent = { text: strippedLine, expansions: [] }; 279 | if (currentDepth === defaultDepth) { 280 | telescopicOut.push(currentContent); 281 | nodeStack[nodeStack.length - 1] = { 282 | ...restLastNode, 283 | telescopicOut, 284 | }; 285 | } else { 286 | telescopicOut[telescopicOut.length - 1].expansions!.push(currentContent); 287 | // add this current one as a replacement to the last upper level one. 288 | const newNode = { 289 | depth: currentDepth, 290 | telescopicOut: [currentContent], 291 | }; 292 | nodeStack.push(newNode); 293 | } 294 | } 295 | 296 | return root; 297 | } 298 | 299 | // Ideally this would not be needed (just used to convert between data structures currently). 300 | function _parseOutputIntoContent( 301 | output: TelescopicOutput, 302 | separator: string = " " 303 | ): Content { 304 | const parseReplacementsFromOutput = (out: TelescopicOutput): Line[] => { 305 | return out.flatMap((line: NewContent) => { 306 | if (!line.expansions?.length) { 307 | return []; 308 | } 309 | const newText = line.expansions.map((line) => line.text).join(separator); 310 | 311 | return [ 312 | { 313 | og: line.text, 314 | new: newText, 315 | replacements: line.expansions?.length 316 | ? parseReplacementsFromOutput(line.expansions) 317 | : [], 318 | }, 319 | ]; 320 | }); 321 | }; 322 | const text = output.map((line: NewContent) => line.text).join(separator); 323 | const replacements: Line[] = parseReplacementsFromOutput(output); 324 | 325 | return { text, replacements }; 326 | } 327 | 328 | // Ideally this would not be needed (just used to convert between data structures currently). 329 | function _parseMarkdownIntoContent( 330 | mdContent: string, 331 | separator: string = " " 332 | ): Content { 333 | const output = _parseMarkdown(mdContent); 334 | return _parseOutputIntoContent(output, separator); 335 | } 336 | 337 | /** 338 | * Parses a markdown-compatible bulleted list into an HTML div that contains the telescoping text specified by the bullet list content. 339 | * 340 | * @param listContent - Content in the form of a bulleted list where items on the same level are combined using the `separator` parameter. 341 | * @param config - Configuration options provided to create interactive, telescopic text. 342 | * @returns HTML div containing the telescoping text. 343 | */ 344 | function createTelescopicTextFromBulletedList( 345 | listContent: string, 346 | { separator = " ", ...createTelescopicTextConfig }: Config = {} 347 | ): HTMLDivElement { 348 | const { textMode = TextMode.Text } = createTelescopicTextConfig; 349 | if (!isTextMode(textMode)) { 350 | throw new Error( 351 | `Invalid textMode provided! Please input one of ${Object.values(TextMode).map( 352 | (s) => `'${s}'` 353 | ).join(", ")}` 354 | ); 355 | } 356 | const content = _parseMarkdownIntoContent(listContent, separator); 357 | return _createTelescopicText([content], { 358 | textMode, 359 | ...createTelescopicTextConfig, 360 | }); 361 | } 362 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var __assign = (this && this.__assign) || function () { 2 | __assign = Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | return __assign.apply(this, arguments); 11 | }; 12 | var __rest = (this && this.__rest) || function (s, e) { 13 | var t = {}; 14 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 15 | t[p] = s[p]; 16 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 17 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 18 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 19 | t[p[i]] = s[p[i]]; 20 | } 21 | return t; 22 | }; 23 | /** 24 | * Modes that designate what form the input text is in and should be interpreted as. 25 | */ 26 | var TextMode; 27 | (function (TextMode) { 28 | TextMode["Text"] = "text"; 29 | /** 30 | * NOTE: this uses `innerHtml` under the hood, so should not be used with untrusted user input and can have 31 | * unexpected behavior if the HTML is not valid. 32 | */ 33 | TextMode["Html"] = "html"; 34 | })(TextMode || (TextMode = {})); 35 | // internal fn to typeguard TextMode 36 | function isTextMode(e) { 37 | return Object.values(TextMode).includes(e); 38 | } 39 | // time; recorded to prevent recursive text expansion on single hover 40 | var _lastHoveredTime = Date.now(); 41 | // By default, only these special characters have text replacements 42 | // - line breaks 43 | // - bold 44 | // - emphasis 45 | var DefaultReplacements = { 46 | // line break 47 | "---": function () { 48 | return document.createElement("hr"); 49 | }, 50 | // bold 51 | "\\*(.*)\\*": function (lineText) { 52 | var el = document.createElement("strong"); 53 | el.appendChild(document.createTextNode(lineText)); 54 | return el; 55 | }, 56 | // emphasis 57 | "_(.*)_": function (lineText) { 58 | var el = document.createElement("em"); 59 | el.appendChild(document.createTextNode(lineText)); 60 | return el; 61 | } 62 | }; 63 | // Internal recursive function to hydrate a node with a line object. 64 | function _hydrate(line, node, config) { 65 | if (config === void 0) { config = {}; } 66 | var shouldExpandOnMouseOver = config.shouldExpandOnMouseOver, _a = config.textMode, textMode = _a === void 0 ? TextMode.Text : _a, _b = config.htmlContainerTag, htmlContainerTag = _b === void 0 ? "span" : _b, _c = config.specialCharacters, specialCharacters = _c === void 0 ? DefaultReplacements : _c; 67 | var lineText = line.text; 68 | function createLineNode(lineText) { 69 | switch (textMode) { 70 | case TextMode.Text: 71 | for (var _i = 0, _a = Object.entries(specialCharacters); _i < _a.length; _i++) { 72 | var _b = _a[_i], specialCharRegex = _b[0], replacementFn = _b[1]; 73 | var matches = lineText.match(specialCharRegex); 74 | if (matches) { 75 | var container = document.createElement(htmlContainerTag); 76 | var wholeMatch = matches[0], innerMatchText = matches[1], _ = matches[2]; 77 | // compute pre and post match text 78 | var _c = lineText.split(wholeMatch), pre = _c[0], post = _c[1]; 79 | container.appendChild(createLineNode(pre)); 80 | container.appendChild(replacementFn(innerMatchText)); 81 | container.appendChild(createLineNode(post)); 82 | return container; 83 | } 84 | } 85 | // leaf, no special characters 86 | return document.createTextNode(lineText); 87 | case TextMode.Html: 88 | var newNode = document.createElement(htmlContainerTag); 89 | newNode.innerHTML = lineText; 90 | return newNode; 91 | default: 92 | throw new Error("Invalid text mode found: " + textMode); 93 | } 94 | } 95 | var _loop_1 = function (i) { 96 | var replacement = line.replacements[i]; 97 | // split single occurrence of replacement pattern 98 | var _d = lineText.split(replacement.og), before_1 = _d[0], after_1 = _d.slice(1); 99 | lineText = after_1.join(replacement.og); 100 | // add old real text 101 | node.appendChild(createLineNode(before_1)); 102 | // create actual telescope 103 | var detail = document.createElement("span"); 104 | detail.classList.add("details", "close"); 105 | // add expand on click handler 106 | detail.addEventListener("click", function () { 107 | detail.classList.remove("close"); 108 | detail.classList.add("open"); 109 | }); 110 | if (shouldExpandOnMouseOver) { 111 | // expand the text if text was not moused over immediately before 112 | detail.addEventListener("mouseover", function () { 113 | if (Date.now() - _lastHoveredTime > 10) { 114 | detail.classList.remove("close"); 115 | detail.classList.add("open"); 116 | _lastHoveredTime = Date.now(); 117 | } 118 | }); 119 | } 120 | var summary = document.createElement("span"); 121 | summary.classList.add("summary"); 122 | var newNode = createLineNode(replacement.og); 123 | summary.appendChild(newNode); 124 | detail.appendChild(summary); 125 | // create inner text, recursively hydrate 126 | var newLine = { 127 | text: replacement.new, 128 | replacements: replacement.replacements, 129 | }; 130 | var expanded = document.createElement("span"); 131 | expanded.classList.add("expanded"); 132 | _hydrate(newLine, expanded, config); 133 | // append to parent 134 | detail.appendChild(expanded); 135 | node.appendChild(detail); 136 | }; 137 | // only iterate lines if there are actually things to replace 138 | for (var i = 0; i < line.replacements.length; i++) { 139 | _loop_1(i); 140 | } 141 | if (lineText) { 142 | // otherwise, this is a leaf node 143 | var newNode = createLineNode(lineText); 144 | node.appendChild(newNode); 145 | } 146 | } 147 | // Helper function to create a new `
` node containing the 148 | // telescoping text. 149 | function _createTelescopicText(content, config) { 150 | if (config === void 0) { config = {}; } 151 | var letter = document.createElement("div"); 152 | letter.id = "telescope"; 153 | content.forEach(function (line) { 154 | var newNode = document.createElement("p"); 155 | _hydrate(line, newNode, config); 156 | letter.appendChild(newNode); 157 | }); 158 | return letter; 159 | } 160 | /*****************/ 161 | /* PARSING LOGIC */ 162 | /*****************/ 163 | // Parses the input string and returns the output as a structured data format. 164 | function _parseMarkdown(mdContent) { 165 | // In future we might want to support full markdown in which case.. 166 | // const html = marked.parse(mdContent); 167 | // convert into jsdom 168 | // const lines = html.split("\n"); 169 | // Also idea for "..." or ellipsis character to repreesnt infinite expansion. 170 | var _a, _b; 171 | var BulletSeparators = ["*", "-", "+"]; 172 | var RegexEscapedBulletSeparators = ["\\*", "-", "\\+"]; 173 | var lines = mdContent.split("\n"); 174 | // NOTE: this should handle normalizing the depth (if its an indented list) 175 | var root = []; 176 | var nodeStack = [{ depth: 0, telescopicOut: root }]; 177 | // This is essentially a trie data structure to parse out all the bullet points 178 | // The algorithm works by assuming that any time you encounter a longer depth than the current one, 179 | // you are moving onto the next line. 180 | var firstNonEmptyLine = lines.find(function (l) { return l.trim().length > 0; }); 181 | var defaultDepth = ((_b = (_a = firstNonEmptyLine === null || firstNonEmptyLine === void 0 ? void 0 : firstNonEmptyLine.match("^\\s*(".concat(RegexEscapedBulletSeparators.join("|"), ")"))) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.length) - 1 || 0; 182 | var _loop_2 = function (line) { 183 | var trimmedLine = line.trim(); 184 | if (!trimmedLine.length) { 185 | return "continue"; 186 | } 187 | // validate that the trimmed line starts with the bullet indicators. 188 | if (!BulletSeparators.some(function (sep) { return trimmedLine.startsWith(sep); })) { 189 | console.log("Invalid line found! ".concat(line)); 190 | return "continue"; 191 | } 192 | // count all spaces in front to get current depth 193 | var currentDepth = line.match("^\\s*(".concat(RegexEscapedBulletSeparators.join("|"), ")"))[0] 194 | .length - 1; 195 | // if greater depth, attach on to the last one 196 | // else you can pop back up to one with 1 less depth 197 | while (nodeStack.length - 1 >= 0 && 198 | currentDepth <= nodeStack[nodeStack.length - 1].depth && 199 | nodeStack[nodeStack.length - 1].depth > 0) { 200 | nodeStack.pop(); 201 | } 202 | var _c = nodeStack[nodeStack.length - 1], telescopicOut = _c.telescopicOut, restLastNode = __rest(_c, ["telescopicOut"]); 203 | var strippedLine = trimmedLine.substring(1).replace(/^\s+/, ""); 204 | // Add current content / node to the stack 205 | var currentContent = { text: strippedLine, expansions: [] }; 206 | if (currentDepth === defaultDepth) { 207 | telescopicOut.push(currentContent); 208 | nodeStack[nodeStack.length - 1] = __assign(__assign({}, restLastNode), { telescopicOut: telescopicOut }); 209 | } 210 | else { 211 | telescopicOut[telescopicOut.length - 1].expansions.push(currentContent); 212 | // add this current one as a replacement to the last upper level one. 213 | var newNode = { 214 | depth: currentDepth, 215 | telescopicOut: [currentContent], 216 | }; 217 | nodeStack.push(newNode); 218 | } 219 | }; 220 | for (var _i = 0, lines_1 = lines; _i < lines_1.length; _i++) { 221 | var line = lines_1[_i]; 222 | _loop_2(line); 223 | } 224 | return root; 225 | } 226 | // Ideally this would not be needed (just used to convert between data structures currently). 227 | function _parseOutputIntoContent(output, separator) { 228 | if (separator === void 0) { separator = " "; } 229 | var parseReplacementsFromOutput = function (out) { 230 | return out.flatMap(function (line) { 231 | var _a, _b; 232 | if (!((_a = line.expansions) === null || _a === void 0 ? void 0 : _a.length)) { 233 | return []; 234 | } 235 | var newText = line.expansions.map(function (line) { return line.text; }).join(separator); 236 | return [ 237 | { 238 | og: line.text, 239 | new: newText, 240 | replacements: ((_b = line.expansions) === null || _b === void 0 ? void 0 : _b.length) 241 | ? parseReplacementsFromOutput(line.expansions) 242 | : [], 243 | }, 244 | ]; 245 | }); 246 | }; 247 | var text = output.map(function (line) { return line.text; }).join(separator); 248 | var replacements = parseReplacementsFromOutput(output); 249 | return { text: text, replacements: replacements }; 250 | } 251 | // Ideally this would not be needed (just used to convert between data structures currently). 252 | function _parseMarkdownIntoContent(mdContent, separator) { 253 | if (separator === void 0) { separator = " "; } 254 | var output = _parseMarkdown(mdContent); 255 | return _parseOutputIntoContent(output, separator); 256 | } 257 | /** 258 | * Parses a markdown-compatible bulleted list into an HTML div that contains the telescoping text specified by the bullet list content. 259 | * 260 | * @param listContent - Content in the form of a bulleted list where items on the same level are combined using the `separator` parameter. 261 | * @param config - Configuration options provided to create interactive, telescopic text. 262 | * @returns HTML div containing the telescoping text. 263 | */ 264 | function createTelescopicTextFromBulletedList(listContent, _a) { 265 | if (_a === void 0) { _a = {}; } 266 | var _b = _a.separator, separator = _b === void 0 ? " " : _b, createTelescopicTextConfig = __rest(_a, ["separator"]); 267 | var _c = createTelescopicTextConfig.textMode, textMode = _c === void 0 ? TextMode.Text : _c; 268 | if (!isTextMode(textMode)) { 269 | throw new Error("Invalid textMode provided! Please input one of ".concat(Object.values(TextMode).map(function (s) { return "'".concat(s, "'"); }).join(", "))); 270 | } 271 | var content = _parseMarkdownIntoContent(listContent, separator); 272 | return _createTelescopicText([content], __assign({ textMode: textMode }, createTelescopicTextConfig)); 273 | } 274 | //# sourceMappingURL=index.js.map --------------------------------------------------------------------------------