├── .gitignore ├── LICENSE ├── README.md ├── debug.log ├── dist ├── daf-renderer.esm.js ├── daf-renderer.min.js └── daf-renderer.umd.js ├── doc-pictures ├── Adding Areas.PNG ├── Area Labels.PNG ├── Comparing Areas.PNG ├── Double-Extend Case.PNG ├── Double-Wrap Case.PNG ├── Spacer Labels.PNG ├── Spacers Together.PNG ├── Spacers.PNG ├── Stairs Case.PNG └── Three Cases.PNG ├── examples ├── edgeCase.html ├── example1.html ├── example2.html ├── fonts │ ├── Mekorot-Rashi.ttf │ ├── Mekorot-Vilna-Bold-Italic.ttf │ ├── Mekorot-Vilna-Bold.ttf │ ├── Mekorot-Vilna-Italic.ttf │ └── Mekorot-Vilna.ttf └── main.js ├── package-lock.json ├── package-lock.json.1784944010 ├── package.json ├── rollup.config.js └── src ├── calculate-spacers-breaks.js ├── calculate-spacers.js ├── options.js ├── renderer.js ├── style-manager.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | experimental 2 | 3 | node_modules 4 | 5 | .idea 6 | 7 | *.code-workspace 8 | /debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dan Jutan Shaun Regenbaum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📜 daf-renderer 📜 2 | A DOM render library for creating Talmud pages on the web - *the library works, but documentation and testing are in progress!* 3 | 4 | The goal of this library is to quickly and easily create an HTML object that represents a natively rendered Talmud page, modeled after the classic [Vilna](https://en.wikipedia.org/wiki/Vilna_Edition_Shas#/media/File:First_page_of_the_first_tractate_of_the_Talmud_(Daf_Beis_of_Maseches_Brachos).jpg). It can also work for any other purposes that require a similar layout. Our library has **no dependencies** and can be used with **two lines of code**. 5 | 6 | ## Installation 7 | 8 | ### Release File 9 | 10 | Download our [release](https://github.com/GT-Jewish-DH/daf-renderer/releases/tag/v0.1) and include the single .js file in your project. 11 | 12 | ### NPM 13 | 14 | If you're using a [module bundler](https://medium.com/@gimenete/how-javascript-bundlers-work-1fc0d0caf2da) for your front-end project, you can install via npm: 15 | 16 | `npm install daf-renderer` 17 | 18 | Then in your code 19 | 20 | `import dafRenderer from "daf-renderer"` 21 | 22 | ## Usage 23 | 24 | 25 | ```javascript 26 | const renderer = dafRenderer("#dafGoesHere", options); 27 | renderer.render(mainHTML, innerHTML, outerHTML, "b"); 28 | ``` 29 | For examples on how to use this, refer to the example files provided. 30 | 31 | *Note: if you're loading fonts to be used in the input texts, make sure these fonts are fully loaded before calling the render method.* 32 | 33 | ### Options (optional) 34 | The renderer can take into account a number of customizable options. If these options are not included, then it will default to what you find below. If you want to change anything, you simply need to pass in an object to the dafRenderer as outlined below with your desired changes. 35 | There are seven options you can change: 36 | 1. *contentWidth*: This option controls how wide the page should be. Everything else will be automatically adapted to fit the content within the designated width. 37 | 2. *mainWidth*: This option controls the percentage of the *contentWidth* which the main body of text takes up. For example, if you set this to 50%, then the main body will take up .5 x *contentWidth*, leaving 25% of the width to each of the side bodies of text. 38 | 3. *padding*: This determines the padding within the rendered object, not outside of it, specifically the padding between the three different bodies of text. Anywhere where two bodies of text are next to one another, this option will control how far apart the two texts are. If there is vertical space, the vertical padding will control how large that space is, and if there is horinzontal space, the horizontal padding will control how large that space is. 39 | 4. *fontFamily*: This option controls the fonts that the renderer uses. You can use any of the standard web fonts, or Rashi and Vilna (which are included). If you would like to use other fonts, simply make sure to include them in your file strucutre and refer to them here. 40 | 5. *direction*: This controls the direction of the text. For English use "ltr", otherwise it will default to "rtl". 41 | 6. *fontSize*: This controls the font size of the different bodies of text. For now, you cannot set the inner and outer font sizes or line heights independently; both the inner and outer side texts use the size specified under `side`. 42 | 7. *lineHeight*: This option controls the vertical spacing between lines within a single body of text. Be careful with making this value too small as it may introduce rendering problems. 43 | 44 | ```javascript 45 | { 46 | contentWidth: "600px", 47 | mainWidth: "50%", 48 | padding: { 49 | vertical: "10px", 50 | horizontal: "16px", 51 | }, 52 | fontFamily: { 53 | inner: "Rashi", 54 | outer: "Rashi", 55 | main: "Vilna" 56 | }, 57 | direction: "rtl", 58 | fontSize: { 59 | main: "15px", 60 | side: "10.5px" 61 | }, 62 | lineHeight: { 63 | main: "17px", 64 | side: "14px", 65 | }, 66 | } 67 | ``` 68 | 69 | ### Data Sources 70 | 71 | #### Sefaria 72 | *Coming Soonish...* 73 | 74 | #### Talmud.dev API 75 | *Coming Soonish...* 76 | 77 | ## How it Works (Better Documenation On the Way) 78 | 79 | ### Spacers and the DOM 80 | The layout of the Talmud is not easily replicated with the box-model of the web. This is because there is no such thing as *middle* for the CSS [float](https://developer.mozilla.org/en-US/docs/Web/CSS/float) property, or any other kind of ability to allow multiple bodies of text to wrap around one another. This limitation was overcome with a new paradigm we call *spacers*. Spacers take advantage of the wrap-around principles of [flow](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Normal_Flow). When a *right-floated* body of text encounters a *left-floated* element, it will wrap around that element instead of overlapping. Thus, we can make complex shapes out of the text by using multiple spacers to force the text into the shape we want: 81 | 82 | ![A picture showing three boxes with text wrapping around them](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Spacers.PNG) 83 | 84 | If we use three layers stacked on top of each other, we can then recreate the page in its entirety: 85 | 86 | ![A picture sketching the spacer over the daf](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Spacers%20Together.PNG) 87 | 88 | We can see above that there are many spacers, but we only need to worry about four of them. We will define them as such: 89 | - Start Spacer 90 | - Inner Spacer 91 | - Outer Spacer 92 | - Bottom Spacer 93 | 94 | Once we have this structure, where there are three layers each with their own spacers, the only thing left is to calculate the dimensions of the spacers listed above. Specifically, it is important to know their heights. 95 | 96 | 97 | ### Algorithm 98 | The algorithim has two stages. 99 | 100 | In order to understand both stages, we must understand that there are three possible layouts: 101 | 102 | - Double-Wrap 103 | - Stairs 104 | - Double-Extend 105 | 106 | ![A picture depicting all three cases](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Three%20Cases.PNG) 107 | 108 | 109 | 110 | The first stage of the algorithim determines which layout the current page is. In order to do this there are three-five steps: 111 | 112 | 1. First we calculate the area that each body of text occupies (in terms of px^2). 113 | 2. Second we divide the calculated area by each text's respective width to get an expected height. 114 | 115 | ![A picture depicting Area](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Area%20Labels.PNG) 116 | 117 | 3. Third we compare these expected heights. If the main text has the smallest height then we know that we are dealing with the case of **Double-Wrap**. 118 | 119 | ![A picture depicting a comparison](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Comparing%20Areas.PNG) 120 | 121 | 4. If the main text is not smallest we then add the areas of the two smallest texts and divide that by their added widths to get a new height. 122 | 123 | ![A picture depicting a comparison](https://github.com/Jutanium/daf-render-lib/blob/master/doc-pictures/Adding%20Areas.PNG) 124 | 125 | 5. We then compare the new height to the largest expected height. If the new height is smaller than the largest expected height then we are dealing with the case of **Stairs**. If not, we are dealing with the case of **Double-Extend**. 126 | 127 | The second stage of the algorithim calculates the spacer heights based on the type of layout the page is. 128 | This stage requires only three things: 129 | - The type of layout 130 | - The calculated area values from before 131 | - The padding values that we can optionally apply 132 | 133 | We will divide the respective calculations into three parts corresponding to the three kinds of layouts: 134 | 135 | 136 | For the case of Double-Wrap: 137 | - Inner Spacer = Main Area / Main Width 138 | - Outer Spacer = Main Area / Main Width 139 | - End Spacer = (Inner or Outer Area - (Main Area / Main Width) * Side Width) / Top Width 140 | 141 | 142 | For the case of Stairs: 143 | - Inner or Outer Spacer = Stair Area / Side Width 144 | - Outer or Inner Spacer = (Main Area + Stair Area - Padding Area) / Combined Width 145 | - Padding Area = (Combined Height - Stair Height) * Horizontal Padding 146 | - End Spacer = 0 147 | 148 | 149 | For the case of Double Extend: 150 | - Inner Spacer = Inner Area / Side Width 151 | - Outer Spacer = Outer Area / Side Width 152 | - End Spacer = 0 153 | 154 | 155 | 156 | 157 | ## License 158 | 159 | MIT License 160 | 161 | Copyright (c) 2020 Dan Jutan Shaun Regenbaum 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy 164 | of this software and associated documentation files (the "Software"), to deal 165 | in the Software without restriction, including without limitation the rights 166 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 167 | copies of the Software, and to permit persons to whom the Software is 168 | furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in all 171 | copies or substantial portions of the Software. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 174 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 175 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 176 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 178 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 179 | SOFTWARE. 180 | 181 | ## Development 182 | Local setup, contributions, issues guidelines etc. 183 | 184 | 185 | -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [0119/094405.534:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 2 | -------------------------------------------------------------------------------- /dist/daf-renderer.esm.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | contentWidth: "600px", 3 | mainWidth: "50%", 4 | padding: { 5 | vertical: "10px", 6 | horizontal: "16px", 7 | }, 8 | innerPadding: "4px", 9 | outerPadding: "4px", 10 | halfway: "50%", 11 | fontFamily: { 12 | inner: "Rashi", 13 | outer: "Rashi", 14 | main: "Vilna" 15 | }, 16 | direction: "rtl", 17 | fontSize: { 18 | main: "15px", 19 | side: "10.5px" 20 | }, 21 | lineHeight: { 22 | main: "17px", 23 | side: "14px", 24 | } 25 | }; 26 | 27 | function mergeAndClone (modified, definitional = defaultOptions) { 28 | const newOptions = {}; 29 | for (const key in definitional) { 30 | if (key in modified) { 31 | const defType = typeof definitional[key]; 32 | if (typeof modified[key] !== defType) { 33 | console.error(`Option ${key} must be of type ${defType}; ${typeof modified[key]} was passed.`); 34 | } 35 | if (defType == "object") { 36 | newOptions[key] = mergeAndClone(modified[key], definitional[key]); 37 | } else { 38 | newOptions[key] = modified[key]; 39 | } 40 | } else { 41 | newOptions[key] = definitional[key]; 42 | } 43 | } 44 | return newOptions; 45 | } 46 | 47 | function getAreaOfText(text, font, fs, width, lh, dummy) { 48 | let testDiv = document.createElement("div"); 49 | testDiv.style.font = String(fs) + "px " + String(font); 50 | testDiv.style.width = String(width) + "px"; //You can remove this, but it may introduce unforseen problems 51 | testDiv.style.lineHeight = String(lh) + "px"; 52 | testDiv.innerHTML = text; 53 | dummy.append(testDiv); 54 | let test_area = Number(testDiv.clientHeight * testDiv.clientWidth); 55 | testDiv.remove(); 56 | return test_area; 57 | } 58 | 59 | function calculateSpacers(mainText, innerText, outerText, options, dummy) { 60 | 61 | const parsedOptions = { 62 | width: parseFloat(options.contentWidth), 63 | padding: { 64 | vertical: parseFloat(options.padding.vertical), 65 | horizontal: parseFloat(options.padding.horizontal) 66 | }, 67 | halfway: 0.01 * parseFloat(options.halfway), 68 | fontFamily: options.fontFamily, 69 | fontSize: { 70 | main: parseFloat(options.fontSize.main), 71 | side: parseFloat(options.fontSize.side), 72 | }, 73 | lineHeight: { 74 | main: parseFloat(options.lineHeight.main), 75 | side: parseFloat(options.lineHeight.side), 76 | }, 77 | mainWidth: 0.01 * parseFloat(options.mainWidth) 78 | }; 79 | 80 | const midWidth = Number(parsedOptions.width * parsedOptions.mainWidth) - 2*parsedOptions.padding.horizontal; //main middle strip 81 | const topWidth = Number(parsedOptions.width * parsedOptions.halfway) - parsedOptions.padding.horizontal; //each commentary top 82 | const sideWidth = Number(parsedOptions.width * (1 - parsedOptions.mainWidth)/2); //each commentary widths, dont include padding, sokeep it constant 83 | 84 | const spacerHeights = { 85 | start: 4.3 * parsedOptions.lineHeight.side, 86 | inner: null, 87 | outer: null, 88 | end: 0, 89 | exception: 0 90 | }; 91 | 92 | // We are accounting for the special case, where you have line breaks: 93 | // if (options.lineBreaks) { 94 | // console.log("Special Case for Line Breaks") 95 | // const main = { 96 | // name: "main", 97 | // text: mainText, 98 | // lineHeight: parsedOptions.lineHeight.main, 99 | // top: 0, 100 | // } 101 | // const outer = { 102 | // name: "outer", 103 | // text: outerText, 104 | // lineHeight: parsedOptions.lineHeight.side, 105 | // top: 4, 106 | // } 107 | // const inner = { 108 | // name: "inner", 109 | // text: innerText, 110 | // lineHeight: parsedOptions.lineHeight.side, 111 | // top: 4, 112 | // } 113 | // 114 | // const texts = [main, outer, inner]; 115 | // texts.forEach(body => body.brCount = (body.text.match(/
/g) || []).length - body.top); 116 | // texts.forEach(body => body.height = (body.brCount * body.lineHeight)); 117 | // texts.forEach(body => body.unadjustedHeight = ((body.brCount + body.top + 1) * body.lineHeight)); 118 | // texts.forEach(body => body.unadjustedHeightAlt = ((body.brCount + body.top) * body.lineHeight)*sideWidth/topWidth); 119 | // const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 120 | // 121 | // const exConst = 2.2 122 | // 123 | // //Checking Exceptions: 124 | // if (inner.unadjustedHeight <= 0 && outer.unadjustedHeight <= 0){ 125 | // console.error("No Commentary"); 126 | // return Error("No Commentary"); 127 | // }; 128 | // if (inner.unadjustedHeightAlt/exConst < spacerHeights.start || outer.unadjustedHeightAlt/exConst < spacerHeights.start) { 129 | // console.log("Exceptions") 130 | // if (inner.unadjustedHeightAlt/exConst <= spacerHeights.start) { 131 | // spacerHeights.inner = inner.unadjustedHeight; 132 | // spacerHeights.outer = outer.height 133 | // return spacerHeights; 134 | // } 135 | // if (outer.unadjustedHeightAlt/exConst <= spacerHeights.start) { 136 | // spacerHeights.outer = outer.unadjustedHeight; 137 | // spacerHeights.inner = inner.height; 138 | // return spacerHeights; 139 | // } 140 | // else { 141 | // return Error("Inner Spacer Error"); 142 | // } 143 | // }; 144 | // //If Double=Wrap 145 | // if (perHeight[0].name === "main"){ 146 | // console.log("Double-Wrap"); 147 | // spacerHeights.inner = main.height; 148 | // spacerHeights.outer = main.height; 149 | // 150 | // const brDifference = perHeight[1].brCount - perHeight[0].brCount; 151 | // spacerHeights.end = brDifference*perHeight[1].lineHeight; 152 | // return spacerHeights; 153 | // } 154 | // 155 | // //If Stairs 156 | // if (perHeight[1].name === "main") { 157 | // console.log("Stairs"); 158 | // spacerHeights[perHeight[0].name] = perHeight[0].height; 159 | // spacerHeights[perHeight[2].name] = main.height; 160 | // return spacerHeights; 161 | // } 162 | // 163 | // //If Double Extend 164 | // console.log("Double-Extend") 165 | // spacerHeights.inner = inner.height + (inner.height/inner.height**2)*inner.lineHeight; 166 | // spacerHeights.outer = outer.height + (inner.height/inner.height**2)*outer.lineHeight; 167 | // return spacerHeights 168 | // } 169 | 170 | 171 | // We could probably put this somewhere else, it was meant to be a place for all the padding corrections, 172 | // but there turned out to only be one 173 | const paddingAreas = { 174 | name: "paddingAreas", 175 | horizontalSide: sideWidth * parsedOptions.padding.vertical, 176 | }; 177 | 178 | 179 | const topArea = (lineHeight) => ((4 * lineHeight * topWidth)); //remove area of the top 4 lines 180 | 181 | 182 | const main = { 183 | name: "main", 184 | width: midWidth, 185 | text: mainText, 186 | lineHeight: parsedOptions.lineHeight.main, 187 | area: getAreaOfText(mainText, parsedOptions.fontFamily.main, parsedOptions.fontSize.main, midWidth, parsedOptions.lineHeight.main, dummy), 188 | length: null, 189 | height: null, 190 | }; 191 | const outer = { 192 | name: "outer", 193 | width: sideWidth, 194 | text: outerText, 195 | lineHeight: parsedOptions.lineHeight.side, 196 | area: getAreaOfText(outerText, parsedOptions.fontFamily.outer, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 197 | - topArea(parsedOptions.lineHeight.side), 198 | length: null, 199 | height: null, 200 | }; 201 | const inner = { 202 | name: "inner", 203 | width: sideWidth, 204 | text: innerText, 205 | lineHeight: parsedOptions.lineHeight.side, 206 | area: 207 | getAreaOfText(innerText, parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 208 | - topArea(parsedOptions.lineHeight.side), 209 | length: null, 210 | height: null, 211 | }; 212 | 213 | const texts = [main, outer, inner]; 214 | texts.forEach(text => text.height = text.area / text.width); 215 | texts.forEach(text => text.unadjustedArea = text.area + topArea(parsedOptions.lineHeight.side)); 216 | texts.forEach(text => text.unadjustedHeight = text.unadjustedArea / text.width); 217 | 218 | const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 219 | 220 | //There are Three Main Types of Case: 221 | //Double-Wrap: The main text being the smallest and commentaries wrapping around it 222 | //Stairs: The main text wrapping around one, but the other wrapping around it 223 | //Double-Extend: The main text wrapping around both commentaries 224 | 225 | //Main Text is Smallest: Double-Wrap 226 | //Main Text being Middle: Stairs 227 | //Main Text Being Largest: Double-Extend 228 | 229 | //First we need to check we have enough commentary to fill the first four lines 230 | if (inner.height <= 0 && outer.height <= 0){ 231 | console.error("No Commentary"); 232 | return Error("No Commentary"); 233 | } 234 | 235 | // This is a case that we have to decice what to do with, when there is not enough commentary on both sides to fill the lines. 236 | if (inner.height <= spacerHeights.start && outer.height <= spacerHeights.start) { 237 | console.error("Not Enough Commentary to Fill Four Lines"); 238 | return Error("Not Enough Commentary"); 239 | } 240 | // We are going to deal with our first edge case when there is either only one commentary 241 | // Or where there is enough of one commentary, but not four lines of the other. 242 | if (inner.unadjustedHeight <= spacerHeights.start || outer.unadjustedHeight <= spacerHeights.start) { 243 | if (inner.unadjustedHeight <= spacerHeights.start) { 244 | spacerHeights.inner = inner.unadjustedHeight; 245 | spacerHeights.outer = (outer.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 246 | spacerHeights.exception = 1; 247 | return spacerHeights; 248 | } 249 | if (outer.unadjustedHeight <= spacerHeights.start) { 250 | spacerHeights.outer = outer.unadjustedHeight; 251 | 252 | spacerHeights.inner = (inner.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 253 | spacerHeights.exception = 2; 254 | return spacerHeights; 255 | } 256 | else { 257 | return Error("Inner Spacer Error"); 258 | } 259 | } 260 | //If Double=Wrap 261 | if (perHeight[0].name === "main"){ 262 | console.log("Double-Wrap"); 263 | spacerHeights.inner = main.area/midWidth; 264 | spacerHeights.outer = spacerHeights.inner; 265 | 266 | const sideArea = spacerHeights.inner * sideWidth + paddingAreas.horizontalSide; 267 | const bottomChunk = perHeight[1].area - sideArea; 268 | const bottomHeight = bottomChunk / topWidth; 269 | spacerHeights.end = bottomHeight; 270 | return spacerHeights; 271 | } 272 | // If Stairs, there's one text at the bottom. We will call it THE stair. 273 | // The remaining two texts form a "block" that we must compare with that bottom text. 274 | const blockArea = (main.area + perHeight[0].area); 275 | const blockWidth = midWidth + sideWidth; 276 | const blockHeight = blockArea / blockWidth; 277 | 278 | const stair = (perHeight[1].name == "main") ? perHeight[2] : perHeight[1]; 279 | const stairHeight = stair.area / stair.width; 280 | 281 | if (blockHeight < stairHeight) { 282 | console.log(`Stairs, ${stair.name} is the stair`); 283 | // This function accounts for extra space that is introduced by padding 284 | const lilArea = (height1, height2, horizPadding) => (horizPadding) * (height1 - height2); 285 | const smallest = perHeight[0]; 286 | spacerHeights[smallest.name] = smallest.height; 287 | spacerHeights[stair.name] = (blockArea - lilArea(blockHeight, spacerHeights[smallest.name], parsedOptions.padding.horizontal)) / blockWidth; 288 | return spacerHeights 289 | } 290 | //If Double Extend 291 | console.log("Double-Extend"); 292 | spacerHeights.inner = inner.height; 293 | spacerHeights.outer = outer.height; 294 | 295 | return spacerHeights 296 | } 297 | 298 | function styleInject(css, ref) { 299 | if ( ref === void 0 ) ref = {}; 300 | var insertAt = ref.insertAt; 301 | 302 | if (!css || typeof document === 'undefined') { return; } 303 | 304 | var head = document.head || document.getElementsByTagName('head')[0]; 305 | var style = document.createElement('style'); 306 | style.type = 'text/css'; 307 | 308 | if (insertAt === 'top') { 309 | if (head.firstChild) { 310 | head.insertBefore(style, head.firstChild); 311 | } else { 312 | head.appendChild(style); 313 | } 314 | } else { 315 | head.appendChild(style); 316 | } 317 | 318 | if (style.styleSheet) { 319 | style.styleSheet.cssText = css; 320 | } else { 321 | style.appendChild(document.createTextNode(css)); 322 | } 323 | } 324 | 325 | var css_248z = "/*Keep this as the first rule in the file*/\n.styles_dafRoot__1QUlM {\n --contentWidth: 0px;\n --padding-horizontal: 0px;\n --padding-vertical: 0px;\n --halfway: 50%;\n\n --fontFamily-inner: \"Rashi\";\n --fontFamily-outer: \"Tosafot\";\n --fontFamily-main: \"Vilna\";\n --direction: \"rtl\";\n\n --fontSize-main: 0px;\n --fontSize-side: 0px;\n\n --lineHeight-main: 0px;\n --lineHeight-side: 0px;\n\n --mainWidth: 0%;\n --mainMargin-start: var(--mainWidth);\n --sidePercent: calc(calc(100% - var(--mainMargin-start)) / 2);\n --remainderPercent: calc(100% - var(--sidePercent));\n\n --innerFloat: left;\n --outerFloat: right;\n\n --spacerHeights-start: 0px;\n --spacerHeights-outer: 0px;\n --spacerHeights-inner: 0px;\n --spacerHeights-end: 0px;\n\n /*Edge Cases*/\n --hasInnerStartGap: 0;\n --hasOuterStartGap: 0;\n --innerStartWidth: 50%;\n --innerPadding: 0px;\n --outerStartWidth: 50%;\n --outerPadding: 0px;\n}\n\n/*Containers*/\n.styles_dafRoot__1QUlM,\n.styles_outer__abXQX,\n.styles_inner__x-amJ,\n.styles_main__BHTRd {\n width: var(--contentWidth);\n pointer-events: none;\n box-sizing: content-box;\n}\n\n.styles_outer__abXQX, .styles_inner__x-amJ, .styles_main__BHTRd {\n position: absolute;\n}\n\n/*Float changes with amud*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n float: var(--innerFloat);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n float: var(--outerFloat);\n}\n\n/*Spacer widths determined by options*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_outer__abXQX .styles_spacer__2T7TS {\n width: var(--halfway);\n}\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n width: var(--remainderPercent);\n}\n\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--contentWidth);\n}\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n width: var(--sidePercent);\n}\n\n/*Spacer heights determined by algorithm*/\n.styles_spacer__2T7TS.styles_start__AwkfY {\n height: var(--spacerHeights-start);\n}\n\n.styles_spacer__2T7TS.styles_end__2wr6A {\n height: var(--spacerHeights-end);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n height: var(--spacerHeights-inner);\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n height: var(--spacerHeights-outer);\n}\n\n/*Settings to handle edge Cases*/\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--innerStartWidth);\n margin-left: var(--innerPadding);\n margin-right: var(--innerPadding);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--outerStartWidth);\n margin-left: var(--outerPadding);\n margin-right: var(--outerPadding);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY{\n margin-bottom: calc(var(--padding-vertical) * var(--hasInnerStartGap));\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n margin-bottom: calc(var(--padding-vertical) * var(--hasOuterStartGap));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n clear: both;\n}\n\n/*Margins!*/\n.styles_spacer__2T7TS.styles_start__AwkfY,\n.styles_spacer__2T7TS.styles_end__2wr6A,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-left: calc(0.5 * var(--padding-horizontal));\n margin-right: calc(0.5 * var(--padding-horizontal));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_text__1_7-z {\n margin-top: var(--padding-vertical);\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-bottom: var(--padding-vertical);\n}\n\n/*Text*/\n.styles_text__1_7-z {\n direction: var(--direction);\n text-align: justify;\n text-align-last: center;\n}\n\n.styles_text__1_7-z span {\n pointer-events: auto;\n}\n\n.styles_main__BHTRd .styles_text__1_7-z {\n font-family: var(--fontFamily-main);\n font-size: var(--fontSize-main);\n line-height: var(--lineHeight-main);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z,\n.styles_outer__abXQX .styles_text__1_7-z {\n font-size: var(--fontSize-side);\n line-height: var(--lineHeight-side);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z {\n font-family: var(--fontFamily-inner);\n}\n\n.styles_outer__abXQX .styles_text__1_7-z {\n font-family: var(--fontFamily-outer);\n}\n"; 326 | var classes = {"dafRoot":"styles_dafRoot__1QUlM","outer":"styles_outer__abXQX","inner":"styles_inner__x-amJ","main":"styles_main__BHTRd","spacer":"styles_spacer__2T7TS","outerMid":"styles_outerMid__2WtcY","innerMid":"styles_innerMid__27MCi","mid":"styles_mid__dcgUr","start":"styles_start__AwkfY","end":"styles_end__2wr6A","text":"styles_text__1_7-z"}; 327 | styleInject(css_248z); 328 | 329 | const sideSpacersClasses = { 330 | start: [classes.spacer, classes.start], 331 | mid: [classes.spacer, classes.mid], 332 | end: [classes.spacer, classes.end] 333 | }; 334 | 335 | const containerClasses = { 336 | el: classes.dafRoot, 337 | outer: { 338 | el: classes.outer, 339 | spacers: sideSpacersClasses, 340 | text: classes.text, 341 | }, 342 | inner: { 343 | el: classes.inner, 344 | spacers: sideSpacersClasses, 345 | text: classes.text, 346 | }, 347 | main: { 348 | el: classes.main, 349 | spacers: { 350 | start: sideSpacersClasses.start, 351 | inner: [classes.spacer, classes.innerMid], 352 | outer: [classes.spacer, classes.outerMid] 353 | }, 354 | text: classes.text 355 | } 356 | }; 357 | 358 | function addClasses(element, classNames) { 359 | if (Array.isArray(classNames)) 360 | element.classList.add(...classNames); 361 | else 362 | element.classList.add(classNames); 363 | } 364 | 365 | function setVars(object, prefix = "") { 366 | const varsRule = Array.prototype.find.call 367 | (document.styleSheets, sheet => sheet.rules[0].selectorText == `.${classes.dafRoot}`) 368 | .rules[0]; 369 | 370 | Object.entries(object).forEach(([key, value]) => { 371 | if (typeof value == "string") { 372 | varsRule.style.setProperty(`--${prefix}${key}`, value); 373 | } else if (typeof value == "object") { 374 | setVars(value, `${key}-`); 375 | } 376 | }); 377 | } 378 | 379 | 380 | let appliedOptions; 381 | var styleManager = { 382 | applyClasses(containers, classesMap = containerClasses) { 383 | for (const key in containers) { 384 | if (key in classesMap) { 385 | const value = classesMap[key]; 386 | if (typeof value === "object" && !Array.isArray(value)) { 387 | this.applyClasses(containers[key], value); 388 | } else { 389 | addClasses(containers[key], value); 390 | } 391 | } 392 | } 393 | }, 394 | updateOptionsVars(options) { 395 | appliedOptions = options; 396 | setVars(options); 397 | }, 398 | updateSpacersVars(spacerHeights) { 399 | setVars( 400 | Object.fromEntries( 401 | Object.entries(spacerHeights).map( 402 | ([key, value]) => ([key, String(value) + 'px'])) 403 | ), 404 | "spacerHeights-" 405 | ); 406 | }, 407 | updateIsAmudB(amudB) { 408 | setVars({ 409 | innerFloat: amudB ? "right" : "left", 410 | outerFloat: amudB ? "left" : "right" 411 | }); 412 | }, 413 | manageExceptions(spacerHeights) { 414 | if (!spacerHeights.exception) { 415 | setVars({ 416 | hasOuterStartGap: "0", 417 | hasInnerStartGap: "0", 418 | outerStartWidth: "50%", 419 | innerStartWidth: "50%", 420 | innerPadding: appliedOptions.innerPadding, 421 | outerPadding: appliedOptions.outerPadding, 422 | }); 423 | return; 424 | } 425 | if (spacerHeights.exception === 1) { 426 | console.log("In Style Exception, Case 1"); 427 | setVars({ 428 | hasInnerStartGap: "1", 429 | innerStartWidth: "100%", 430 | outerStartWidth: "0%", 431 | innerPadding: "0px", 432 | outerPadding: "0px", 433 | }); 434 | } else if (spacerHeights.exception === 2) { 435 | console.log("In Style Exception, Case 2"); 436 | setVars({ 437 | hasOuterStartGap: "1", 438 | outerStartWidth: "100%", 439 | innerStartWidth: "0%", 440 | innerPadding: "0px", 441 | outerPadding: "0px" 442 | }); 443 | } 444 | } 445 | }; 446 | 447 | function getLineInfo(text, font, fontSize, lineHeight, dummy) { 448 | dummy.innerHTML = ""; 449 | let testDiv = document.createElement("span"); 450 | testDiv.style.font = fontSize + " " + String(font); 451 | testDiv.style.lineHeight = String(lineHeight) + "px"; 452 | testDiv.innerHTML = text; 453 | testDiv.style.position = "absolute"; 454 | dummy.append(testDiv); 455 | const rect = testDiv.getBoundingClientRect(); 456 | const height = rect.height; 457 | const width = rect.width; 458 | const widthProportional = width / dummy.getBoundingClientRect().width; 459 | testDiv.remove(); 460 | return {height, width, widthProportional}; 461 | } 462 | 463 | function heightAccumulator(font, fontSize, lineHeight, dummy) { 464 | return (lines) => { 465 | return getLineInfo(lines.join("
"), font, fontSize, lineHeight, dummy).height; 466 | } 467 | } 468 | 469 | function getBreaks(sizeArray) { 470 | const widths = sizeArray.map(size => size.widthProportional); 471 | const diffs = widths.map((width, index, widths) => index == 0 ? 0 : Math.abs(width - widths[index - 1])); 472 | const threshold = 0.12; 473 | let criticalPoints = diffs.reduce((indices, curr, currIndex) => { 474 | //Breaks before line 4 are flukes 475 | if (currIndex < 4) return indices; 476 | if (curr > threshold) { 477 | //There should never be two breakpoints in a row 478 | const prevIndex = indices[indices.length - 1]; 479 | if (prevIndex && (currIndex - prevIndex) == 1) { 480 | return indices; 481 | } 482 | indices.push(currIndex); 483 | } 484 | return indices; 485 | }, []); 486 | const averageAround = points => points.map((point, i) => { 487 | let nextPoint; 488 | if (!nextPoint) { 489 | nextPoint = Math.min(point + 3, widths.length - 1); 490 | } 491 | let prevPoint; 492 | if (!prevPoint) { 493 | prevPoint = Math.max(point - 3, 0); 494 | } 495 | /* 496 | Note that these are divided by the width of the critical point line such that 497 | we get the average width of the preceeding and proceeding chunks *relative* 498 | to the critical line. 499 | */ 500 | const before = (widths.slice(prevPoint, point).reduce((acc, curr) => acc + curr) / 501 | (point - prevPoint)) / widths[point]; 502 | let after; 503 | if ( point + 1 >= nextPoint) { 504 | after = widths[nextPoint] / widths[point]; 505 | } else { 506 | after =(widths.slice(point + 1, nextPoint).reduce((acc, curr) => acc + curr) / 507 | (nextPoint - point - 1)) / widths[point]; 508 | } 509 | return { 510 | point, 511 | before, 512 | after, 513 | diff: Math.abs(after - before) 514 | } 515 | }); 516 | const aroundDiffs = averageAround(criticalPoints) 517 | .sort( (a,b) => b.diff - a.diff); 518 | criticalPoints = aroundDiffs 519 | .filter( ({diff}) => diff > 0.22) 520 | .map( ({point}) => point); 521 | return criticalPoints.sort( (a, b) => a - b); 522 | } 523 | 524 | function onlyOneCommentary(lines, options, dummy) { 525 | const fontFamily = options.fontFamily.inner; 526 | const fontSize = options.fontSize.side; 527 | const lineHeight = parseFloat(options.lineHeight.side); 528 | const sizes = lines.map(text => getLineInfo(text, fontFamily, fontSize, lineHeight, dummy)); 529 | const breaks = getBreaks(sizes); 530 | if (breaks.length == 3) { 531 | const first = lines.slice(0, breaks[1]); 532 | const second = lines.slice(breaks[1]); 533 | return [first, second]; 534 | } 535 | } 536 | 537 | function calculateSpacersBreaks(mainArray, rashiArray, tosafotArray, options, dummy) { 538 | const lines = { 539 | main: mainArray, 540 | rashi: rashiArray, 541 | tosafot: tosafotArray 542 | }; 543 | 544 | const parsedOptions = { 545 | padding: { 546 | vertical: parseFloat(options.padding.vertical), 547 | horizontal: parseFloat(options.padding.horizontal) 548 | }, 549 | halfway: 0.01 * parseFloat(options.halfway), 550 | fontFamily: options.fontFamily, // Object of strings 551 | fontSize: { 552 | main: options.fontSize.main, 553 | side: options.fontSize.side, 554 | }, 555 | lineHeight: { 556 | main: parseFloat(options.lineHeight.main), 557 | side: parseFloat(options.lineHeight.side), 558 | }, 559 | }; 560 | 561 | 562 | const mainOptions = [parsedOptions.fontFamily.main, parsedOptions.fontSize.main, parsedOptions.lineHeight.main]; 563 | const commentaryOptions = [parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, parsedOptions.lineHeight.side]; 564 | 565 | const sizes = {}; 566 | sizes.main = lines.main.map(text => getLineInfo(text, ...mainOptions, dummy)); 567 | ["rashi", "tosafot"].forEach(text => { 568 | sizes[text] = lines[text].map(line => getLineInfo(line, ...commentaryOptions, dummy)); 569 | }); 570 | 571 | const accumulateMain = heightAccumulator(...mainOptions, dummy); 572 | const accumulateCommentary = heightAccumulator(...commentaryOptions, dummy); 573 | 574 | const breaks = {}; 575 | 576 | ["rashi", "tosafot", "main"].forEach(text => { 577 | breaks[text] = getBreaks(sizes[text]) 578 | /* 579 | Hadran lines aren't real candidates for line breaks. 580 | TODO: Extract this behavior , give it an option/parameter 581 | */ 582 | .filter(lineNum => !(lines[text][lineNum].includes("hadran")) 583 | ); 584 | }); 585 | 586 | 587 | const spacerHeights = { 588 | start: 4.4 * parsedOptions.lineHeight.side, 589 | inner: null, 590 | outer: null, 591 | end: 0, 592 | exception: 0 593 | }; 594 | 595 | const mainHeight = accumulateMain(lines.main); 596 | const mainHeightOld = (sizes.main.length) * parsedOptions.lineHeight.main; 597 | let afterBreak = { 598 | inner: accumulateCommentary(lines.rashi.slice(4)), 599 | outer: accumulateCommentary(lines.tosafot.slice(4)) 600 | }; 601 | 602 | let afterBreakOld = { 603 | inner: parsedOptions.lineHeight.side * (sizes.rashi.length - 4), 604 | outer: parsedOptions.lineHeight.side * (sizes.tosafot.length - 4) 605 | }; 606 | 607 | if (breaks.rashi.length < 1 || breaks.tosafot.length < 1) { 608 | console.log("Dealing with Exceptions"); 609 | if (breaks.rashi.length < 1) { 610 | afterBreak.inner = parsedOptions.lineHeight.side * (sizes.rashi.length + 1); 611 | spacerHeights.exception = 2; 612 | } 613 | if (breaks.tosafot.length < 1) { 614 | afterBreak.outer = parsedOptions.lineHeight.side * (sizes.tosafot.length + 1); 615 | spacerHeights.exception = 2; 616 | } 617 | } 618 | switch (breaks.main.length) { 619 | case 0: 620 | spacerHeights.inner = mainHeight; 621 | spacerHeights.outer = mainHeight; 622 | if (breaks.rashi.length == 2) { 623 | spacerHeights.end = accumulateCommentary(lines.rashi.slice(breaks.rashi[1])); 624 | } else { 625 | spacerHeights.end = accumulateCommentary(lines.tosafot.slice(breaks.tosafot[1])); 626 | } 627 | console.log("Double wrap"); 628 | break; 629 | case 1: 630 | if (breaks.rashi.length != breaks.tosafot.length) { 631 | if (breaks.tosafot.length == 0) { 632 | spacerHeights.outer = 0; 633 | spacerHeights.inner = afterBreak.inner; 634 | break; 635 | } 636 | if (breaks.rashi.length == 0) { 637 | spacerHeights.inner = 0; 638 | spacerHeights.outer = afterBreak.outer; 639 | break; 640 | } 641 | let stair; 642 | let nonstair; 643 | if (breaks.rashi.length == 1) { 644 | stair = "outer"; 645 | nonstair = "inner"; 646 | } else { 647 | stair = "inner"; 648 | nonstair = "outer"; 649 | } 650 | spacerHeights[nonstair] = afterBreak[nonstair]; 651 | spacerHeights[stair] = mainHeight; 652 | console.log("Stairs"); 653 | break; 654 | } 655 | case 2: 656 | spacerHeights.inner = afterBreak.inner; 657 | spacerHeights.outer = afterBreak.outer; 658 | console.log("Double Extend"); 659 | break; 660 | default: 661 | spacerHeights.inner = afterBreak.inner; 662 | spacerHeights.outer = afterBreak.outer; 663 | console.log("No Case Exception"); 664 | break; 665 | } 666 | return spacerHeights; 667 | } 668 | 669 | function el(tag, parent) { 670 | const newEl = document.createElement(tag); 671 | if (parent) parent.append(newEl); 672 | return newEl; 673 | } 674 | 675 | function div(parent) { 676 | return el("div", parent); 677 | } 678 | 679 | function span(parent) { 680 | return el("span", parent); 681 | } 682 | 683 | 684 | function renderer (el, options = defaultOptions) { 685 | const root = (typeof el === "string") ? document.querySelector(el) : el; 686 | if (!(root && root instanceof Element && root.tagName.toUpperCase() === "DIV")) { 687 | throw "Argument must be a div element or its selector" 688 | } 689 | const outerContainer = div(root); 690 | const innerContainer = div(root); 691 | const mainContainer = div(root); 692 | const dummy = div(root); 693 | dummy.id = "dummy"; 694 | const containers = { 695 | el: root, 696 | dummy: dummy, 697 | outer: { 698 | el: outerContainer, 699 | spacers: { 700 | start: div(outerContainer), 701 | mid: div(outerContainer), 702 | end: div(outerContainer) 703 | }, 704 | text: div(outerContainer) 705 | }, 706 | inner: { 707 | el: innerContainer, 708 | spacers: { 709 | start: div(innerContainer), 710 | mid: div(innerContainer), 711 | end: div(innerContainer) 712 | }, 713 | text: div(innerContainer) 714 | }, 715 | main: { 716 | el: mainContainer, 717 | spacers: { 718 | start: div(mainContainer), 719 | inner: div(mainContainer), 720 | outer: div(mainContainer), 721 | }, 722 | text: div(mainContainer) 723 | } 724 | }; 725 | 726 | const textSpans = { 727 | main: span(containers.main.text), 728 | inner: span(containers.inner.text), 729 | outer: span(containers.outer.text) 730 | }; 731 | 732 | const clonedOptions = mergeAndClone(options, defaultOptions); 733 | 734 | styleManager.applyClasses(containers); 735 | styleManager.updateOptionsVars(clonedOptions); 736 | 737 | let resizeEvent; 738 | return { 739 | classes, 740 | containers, 741 | spacerHeights: { 742 | start: 0, 743 | inner: 0, 744 | outer: 0, 745 | end: 0 746 | }, 747 | amud: "a", 748 | render(main, inner, outer, amud = "a", linebreak, renderCallback, resizeCallback) { 749 | if (resizeEvent) { 750 | window.removeEventListener("resize", resizeEvent); 751 | } 752 | if (this.amud != amud) { 753 | this.amud = amud; 754 | styleManager.updateIsAmudB(amud == "b"); 755 | } 756 | if (!linebreak) { 757 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 758 | resizeEvent = () => { 759 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 760 | styleManager.updateSpacersVars(this.spacerHeights); 761 | console.log("resizing"); 762 | if (resizeCallback) 763 | resizeCallback(); 764 | }; 765 | window.addEventListener("resize", resizeEvent); 766 | } 767 | else { 768 | let [mainSplit, innerSplit, outerSplit] = [main, inner, outer].map( text => { 769 | containers.dummy.innerHTML = text; 770 | const divRanges = Array.from(containers.dummy.querySelectorAll("div")).map(div => { 771 | const range = document.createRange(); 772 | range.selectNode(div); 773 | return range; 774 | }); 775 | 776 | const brs = containers.dummy.querySelectorAll(linebreak); 777 | const splitFragments = []; 778 | brs.forEach((node, index) => { 779 | const range = document.createRange(); 780 | range.setEndBefore(node); 781 | if (index == 0) { 782 | range.setStart(containers.dummy, 0); 783 | } else { 784 | const prev = brs[index - 1]; 785 | range.setStartAfter(prev); 786 | } 787 | divRanges.forEach( (divRange, i) => { 788 | const inBetween = range.compareBoundaryPoints(Range.START_TO_START, divRange) < 0 && range.compareBoundaryPoints(Range.END_TO_END, divRange) > 0; 789 | if (inBetween) { 790 | splitFragments.push(divRange.extractContents()); 791 | divRanges.splice(i, 1); 792 | } 793 | }); 794 | 795 | splitFragments.push(range.extractContents()); 796 | }); 797 | 798 | return splitFragments.map(fragment => { 799 | const el = document.createElement("div"); 800 | el.append(fragment); 801 | return el.innerHTML; 802 | }) 803 | }); 804 | 805 | containers.dummy.innerHTML = ""; 806 | 807 | const hasInner = innerSplit.length != 0; 808 | const hasOuter = outerSplit.length != 0; 809 | 810 | if (hasInner != hasOuter) { 811 | const withText = hasInner ? innerSplit : outerSplit; 812 | const fixed = onlyOneCommentary(withText, clonedOptions, dummy); 813 | if (fixed) { 814 | if (amud == "a") { 815 | innerSplit = fixed[0]; 816 | outerSplit = fixed[1]; 817 | } else { 818 | innerSplit = fixed[1]; 819 | outerSplit = fixed[0]; 820 | } 821 | inner = innerSplit.join('
'); 822 | outer = outerSplit.join('
'); 823 | } 824 | } 825 | 826 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 827 | resizeEvent = () => { 828 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 829 | styleManager.updateSpacersVars(this.spacerHeights); 830 | if (resizeCallback) 831 | resizeCallback(); 832 | console.log("resizing"); 833 | }; 834 | window.addEventListener('resize', resizeEvent); 835 | } 836 | styleManager.updateSpacersVars(this.spacerHeights); 837 | styleManager.manageExceptions(this.spacerHeights); 838 | textSpans.main.innerHTML = main; 839 | textSpans.inner.innerHTML = inner; 840 | textSpans.outer.innerHTML = outer; 841 | 842 | const containerHeight = Math.max(...["main", "inner", "outer"].map(t => containers[t].el.offsetHeight)); 843 | containers.el.style.height = `${containerHeight}px`; 844 | if (renderCallback) 845 | renderCallback(); 846 | }, 847 | } 848 | } 849 | 850 | export default renderer; 851 | -------------------------------------------------------------------------------- /dist/daf-renderer.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).dafRenderer=t()}(this,(function(){"use strict";const e={contentWidth:"600px",mainWidth:"50%",padding:{vertical:"10px",horizontal:"16px"},innerPadding:"4px",outerPadding:"4px",halfway:"50%",fontFamily:{inner:"Rashi",outer:"Rashi",main:"Vilna"},direction:"rtl",fontSize:{main:"15px",side:"10.5px"},lineHeight:{main:"17px",side:"14px"}};function t(n,i=e){const a={};for(const e in i)if(e in n){const s=typeof i[e];typeof n[e]!==s&&console.error(`Option ${e} must be of type ${s}; ${typeof n[e]} was passed.`),a[e]="object"==s?t(n[e],i[e]):n[e]}else a[e]=i[e];return a}function n(e,t,n,i,a,s){let r=document.createElement("div");r.style.font=String(n)+"px "+String(t),r.style.width=String(i)+"px",r.style.lineHeight=String(a)+"px",r.innerHTML=e,s.append(r);let o=Number(r.clientHeight*r.clientWidth);return r.remove(),o}function i(e,t,i,a,s){const r={width:parseFloat(a.contentWidth),padding:{vertical:parseFloat(a.padding.vertical),horizontal:parseFloat(a.padding.horizontal)},halfway:.01*parseFloat(a.halfway),fontFamily:a.fontFamily,fontSize:{main:parseFloat(a.fontSize.main),side:parseFloat(a.fontSize.side)},lineHeight:{main:parseFloat(a.lineHeight.main),side:parseFloat(a.lineHeight.side)},mainWidth:.01*parseFloat(a.mainWidth)},o=Number(r.width*r.mainWidth)-2*r.padding.horizontal,l=Number(r.width*r.halfway)-r.padding.horizontal,d=Number(r.width*(1-r.mainWidth)/2),_={start:4.3*r.lineHeight.side,inner:null,outer:null,end:0,exception:0},h=d*r.padding.vertical,c=e=>4*e*l,p={name:"main",width:o,text:e,lineHeight:r.lineHeight.main,area:n(e,r.fontFamily.main,r.fontSize.main,o,r.lineHeight.main,s),length:null,height:null},m={name:"outer",width:d,text:i,lineHeight:r.lineHeight.side,area:n(i,r.fontFamily.outer,r.fontSize.side,d,r.lineHeight.side,s)-c(r.lineHeight.side),length:null,height:null},u={name:"inner",width:d,text:t,lineHeight:r.lineHeight.side,area:n(t,r.fontFamily.inner,r.fontSize.side,d,r.lineHeight.side,s)-c(r.lineHeight.side),length:null,height:null},g=[p,m,u];g.forEach((e=>e.height=e.area/e.width)),g.forEach((e=>e.unadjustedArea=e.area+c(r.lineHeight.side))),g.forEach((e=>e.unadjustedHeight=e.unadjustedArea/e.width));const y=Array.from(g).sort(((e,t)=>e.height-t.height));if(u.height<=0&&m.height<=0)return console.error("No Commentary"),Error("No Commentary");if(u.height<=_.start&&m.height<=_.start)return console.error("Not Enough Commentary to Fill Four Lines"),Error("Not Enough Commentary");if(u.unadjustedHeight<=_.start||m.unadjustedHeight<=_.start)return u.unadjustedHeight<=_.start?(_.inner=u.unadjustedHeight,_.outer=(m.unadjustedArea-4*r.width*r.lineHeight.side)/d,_.exception=1,_):m.unadjustedHeight<=_.start?(_.outer=m.unadjustedHeight,_.inner=(u.unadjustedArea-4*r.width*r.lineHeight.side)/d,_.exception=2,_):Error("Inner Spacer Error");if("main"===y[0].name){console.log("Double-Wrap"),_.inner=p.area/o,_.outer=_.inner;const e=_.inner*d+h,t=(y[1].area-e)/l;return _.end=t,_}const f=p.area+y[0].area,x=o+d,T=f/x,S="main"==y[1].name?y[2]:y[1];if(Tn*(e-t),t=y[0];return _[t.name]=t.height,_[S.name]=(f-e(T,_[t.name],r.padding.horizontal))/x,_}return console.log("Double-Extend"),_.inner=u.height,_.outer=m.height,_}var a={dafRoot:"styles_dafRoot__1QUlM",outer:"styles_outer__abXQX",inner:"styles_inner__x-amJ",main:"styles_main__BHTRd",spacer:"styles_spacer__2T7TS",outerMid:"styles_outerMid__2WtcY",innerMid:"styles_innerMid__27MCi",mid:"styles_mid__dcgUr",start:"styles_start__AwkfY",end:"styles_end__2wr6A",text:"styles_text__1_7-z"};!function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var i=document.head||document.getElementsByTagName("head")[0],a=document.createElement("style");a.type="text/css","top"===n&&i.firstChild?i.insertBefore(a,i.firstChild):i.appendChild(a),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(document.createTextNode(e))}}('/*Keep this as the first rule in the file*/\n.styles_dafRoot__1QUlM {\n --contentWidth: 0px;\n --padding-horizontal: 0px;\n --padding-vertical: 0px;\n --halfway: 50%;\n\n --fontFamily-inner: "Rashi";\n --fontFamily-outer: "Tosafot";\n --fontFamily-main: "Vilna";\n --direction: "rtl";\n\n --fontSize-main: 0px;\n --fontSize-side: 0px;\n\n --lineHeight-main: 0px;\n --lineHeight-side: 0px;\n\n --mainWidth: 0%;\n --mainMargin-start: var(--mainWidth);\n --sidePercent: calc(calc(100% - var(--mainMargin-start)) / 2);\n --remainderPercent: calc(100% - var(--sidePercent));\n\n --innerFloat: left;\n --outerFloat: right;\n\n --spacerHeights-start: 0px;\n --spacerHeights-outer: 0px;\n --spacerHeights-inner: 0px;\n --spacerHeights-end: 0px;\n\n /*Edge Cases*/\n --hasInnerStartGap: 0;\n --hasOuterStartGap: 0;\n --innerStartWidth: 50%;\n --innerPadding: 0px;\n --outerStartWidth: 50%;\n --outerPadding: 0px;\n}\n\n/*Containers*/\n.styles_dafRoot__1QUlM,\n.styles_outer__abXQX,\n.styles_inner__x-amJ,\n.styles_main__BHTRd {\n width: var(--contentWidth);\n pointer-events: none;\n box-sizing: content-box;\n}\n\n.styles_outer__abXQX, .styles_inner__x-amJ, .styles_main__BHTRd {\n position: absolute;\n}\n\n/*Float changes with amud*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n float: var(--innerFloat);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n float: var(--outerFloat);\n}\n\n/*Spacer widths determined by options*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_outer__abXQX .styles_spacer__2T7TS {\n width: var(--halfway);\n}\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n width: var(--remainderPercent);\n}\n\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--contentWidth);\n}\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n width: var(--sidePercent);\n}\n\n/*Spacer heights determined by algorithm*/\n.styles_spacer__2T7TS.styles_start__AwkfY {\n height: var(--spacerHeights-start);\n}\n\n.styles_spacer__2T7TS.styles_end__2wr6A {\n height: var(--spacerHeights-end);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n height: var(--spacerHeights-inner);\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n height: var(--spacerHeights-outer);\n}\n\n/*Settings to handle edge Cases*/\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--innerStartWidth);\n margin-left: var(--innerPadding);\n margin-right: var(--innerPadding);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--outerStartWidth);\n margin-left: var(--outerPadding);\n margin-right: var(--outerPadding);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY{\n margin-bottom: calc(var(--padding-vertical) * var(--hasInnerStartGap));\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n margin-bottom: calc(var(--padding-vertical) * var(--hasOuterStartGap));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n clear: both;\n}\n\n/*Margins!*/\n.styles_spacer__2T7TS.styles_start__AwkfY,\n.styles_spacer__2T7TS.styles_end__2wr6A,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-left: calc(0.5 * var(--padding-horizontal));\n margin-right: calc(0.5 * var(--padding-horizontal));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_text__1_7-z {\n margin-top: var(--padding-vertical);\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-bottom: var(--padding-vertical);\n}\n\n/*Text*/\n.styles_text__1_7-z {\n direction: var(--direction);\n text-align: justify;\n text-align-last: center;\n}\n\n.styles_text__1_7-z span {\n pointer-events: auto;\n}\n\n.styles_main__BHTRd .styles_text__1_7-z {\n font-family: var(--fontFamily-main);\n font-size: var(--fontSize-main);\n line-height: var(--lineHeight-main);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z,\n.styles_outer__abXQX .styles_text__1_7-z {\n font-size: var(--fontSize-side);\n line-height: var(--lineHeight-side);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z {\n font-family: var(--fontFamily-inner);\n}\n\n.styles_outer__abXQX .styles_text__1_7-z {\n font-family: var(--fontFamily-outer);\n}\n');const s={start:[a.spacer,a.start],mid:[a.spacer,a.mid],end:[a.spacer,a.end]},r={el:a.dafRoot,outer:{el:a.outer,spacers:s,text:a.text},inner:{el:a.inner,spacers:s,text:a.text},main:{el:a.main,spacers:{start:s.start,inner:[a.spacer,a.innerMid],outer:[a.spacer,a.outerMid]},text:a.text}};function o(e,t=""){const n=Array.prototype.find.call(document.styleSheets,(e=>e.rules[0].selectorText==`.${a.dafRoot}`)).rules[0];Object.entries(e).forEach((([e,i])=>{"string"==typeof i?n.style.setProperty(`--${t}${e}`,i):"object"==typeof i&&o(i,`${e}-`)}))}let l;var d={applyClasses(e,t=r){for(const a in e)if(a in t){const s=t[a];"object"!=typeof s||Array.isArray(s)?(n=e[a],i=s,Array.isArray(i)?n.classList.add(...i):n.classList.add(i)):this.applyClasses(e[a],s)}var n,i},updateOptionsVars(e){l=e,o(e)},updateSpacersVars(e){o(Object.fromEntries(Object.entries(e).map((([e,t])=>[e,String(t)+"px"]))),"spacerHeights-")},updateIsAmudB(e){o({innerFloat:e?"right":"left",outerFloat:e?"left":"right"})},manageExceptions(e){e.exception?1===e.exception?(console.log("In Style Exception, Case 1"),o({hasInnerStartGap:"1",innerStartWidth:"100%",outerStartWidth:"0%",innerPadding:"0px",outerPadding:"0px"})):2===e.exception&&(console.log("In Style Exception, Case 2"),o({hasOuterStartGap:"1",outerStartWidth:"100%",innerStartWidth:"0%",innerPadding:"0px",outerPadding:"0px"})):o({hasOuterStartGap:"0",hasInnerStartGap:"0",outerStartWidth:"50%",innerStartWidth:"50%",innerPadding:l.innerPadding,outerPadding:l.outerPadding})}};function _(e,t,n,i,a){a.innerHTML="";let s=document.createElement("span");s.style.font=n+" "+String(t),s.style.lineHeight=String(i)+"px",s.innerHTML=e,s.style.position="absolute",a.append(s);const r=s.getBoundingClientRect(),o=r.height,l=r.width,d=l/a.getBoundingClientRect().width;return s.remove(),{height:o,width:l,widthProportional:d}}function h(e,t,n,i){return a=>_(a.join("
"),e,t,n,i).height}function c(e){const t=e.map((e=>e.widthProportional)),n=t.map(((e,t,n)=>0==t?0:Math.abs(e-n[t-1])));let i=n.reduce(((e,t,n)=>{if(n<4)return e;if(t>.12){const t=e[e.length-1];if(t&&n-t==1)return e;e.push(n)}return e}),[]);var a;return i=(a=i,a.map(((e,n)=>{let i,a;i||(i=Math.min(e+3,t.length-1)),a||(a=Math.max(e-3,0));const s=t.slice(a,e).reduce(((e,t)=>e+t))/(e-a)/t[e];let r;return r=e+1>=i?t[i]/t[e]:t.slice(e+1,i).reduce(((e,t)=>e+t))/(i-e-1)/t[e],{point:e,before:s,after:r,diff:Math.abs(r-s)}}))).sort(((e,t)=>t.diff-e.diff)).filter((({diff:e})=>e>.22)).map((({point:e})=>e)),i.sort(((e,t)=>e-t))}function p(e,t,n,i,a){const s={main:e,rashi:t,tosafot:n},r={padding:{vertical:parseFloat(i.padding.vertical),horizontal:parseFloat(i.padding.horizontal)},halfway:.01*parseFloat(i.halfway),fontFamily:i.fontFamily,fontSize:{main:i.fontSize.main,side:i.fontSize.side},lineHeight:{main:parseFloat(i.lineHeight.main),side:parseFloat(i.lineHeight.side)}},o=[r.fontFamily.main,r.fontSize.main,r.lineHeight.main],l=[r.fontFamily.inner,r.fontSize.side,r.lineHeight.side],d={};d.main=s.main.map((e=>_(e,...o,a))),["rashi","tosafot"].forEach((e=>{d[e]=s[e].map((e=>_(e,...l,a)))}));const p=h(...o,a),m=h(...l,a),u={};["rashi","tosafot","main"].forEach((e=>{u[e]=c(d[e]).filter((t=>!s[e][t].includes("hadran")))}));const g={start:4.4*r.lineHeight.side,inner:null,outer:null,end:0,exception:0},y=p(s.main);d.main.length,r.lineHeight.main;let f={inner:m(s.rashi.slice(4)),outer:m(s.tosafot.slice(4))};r.lineHeight.side,d.rashi.length,r.lineHeight.side,d.tosafot.length;switch((u.rashi.length<1||u.tosafot.length<1)&&(console.log("Dealing with Exceptions"),u.rashi.length<1&&(f.inner=r.lineHeight.side*(d.rashi.length+1),g.exception=2),u.tosafot.length<1&&(f.outer=r.lineHeight.side*(d.tosafot.length+1),g.exception=2)),u.main.length){case 0:g.inner=y,g.outer=y,2==u.rashi.length?g.end=m(s.rashi.slice(u.rashi[1])):g.end=m(s.tosafot.slice(u.tosafot[1])),console.log("Double wrap");break;case 1:if(u.rashi.length!=u.tosafot.length){if(0==u.tosafot.length){g.outer=0,g.inner=f.inner;break}if(0==u.rashi.length){g.inner=0,g.outer=f.outer;break}let e,t;1==u.rashi.length?(e="outer",t="inner"):(e="inner",t="outer"),g[t]=f[t],g[e]=y,console.log("Stairs");break}case 2:g.inner=f.inner,g.outer=f.outer,console.log("Double Extend");break;default:g.inner=f.inner,g.outer=f.outer,console.log("No Case Exception")}return g}function m(e,t){const n=document.createElement(e);return t&&t.append(n),n}function u(e){return m("div",e)}function g(e){return m("span",e)}return function(n,s=e){const r="string"==typeof n?document.querySelector(n):n;if(!(r&&r instanceof Element&&"DIV"===r.tagName.toUpperCase()))throw"Argument must be a div element or its selector";const o=u(r),l=u(r),h=u(r),m=u(r);m.id="dummy";const y={el:r,dummy:m,outer:{el:o,spacers:{start:u(o),mid:u(o),end:u(o)},text:u(o)},inner:{el:l,spacers:{start:u(l),mid:u(l),end:u(l)},text:u(l)},main:{el:h,spacers:{start:u(h),inner:u(h),outer:u(h)},text:u(h)}},f={main:g(y.main.text),inner:g(y.inner.text),outer:g(y.outer.text)},x=t(s,e);let T;return d.applyClasses(y),d.updateOptionsVars(x),{classes:a,containers:y,spacerHeights:{start:0,inner:0,outer:0,end:0},amud:"a",render(e,t,n,a="a",s,r,o){if(T&&window.removeEventListener("resize",T),this.amud!=a&&(this.amud=a,d.updateIsAmudB("b"==a)),s){let[i,r,l]=[e,t,n].map((e=>{y.dummy.innerHTML=e;const t=Array.from(y.dummy.querySelectorAll("div")).map((e=>{const t=document.createRange();return t.selectNode(e),t})),n=y.dummy.querySelectorAll(s),i=[];return n.forEach(((e,a)=>{const s=document.createRange();if(s.setEndBefore(e),0==a)s.setStart(y.dummy,0);else{const e=n[a-1];s.setStartAfter(e)}t.forEach(((e,n)=>{s.compareBoundaryPoints(Range.START_TO_START,e)<0&&s.compareBoundaryPoints(Range.END_TO_END,e)>0&&(i.push(e.extractContents()),t.splice(n,1))})),i.push(s.extractContents())})),i.map((e=>{const t=document.createElement("div");return t.append(e),t.innerHTML}))}));y.dummy.innerHTML="";const h=0!=r.length;if(h!=(0!=l.length)){const e=function(e,t,n){const i=t.fontFamily.inner,a=t.fontSize.side,s=parseFloat(t.lineHeight.side),r=c(e.map((e=>_(e,i,a,s,n))));if(3==r.length)return[e.slice(0,r[1]),e.slice(r[1])]}(h?r:l,x,m);e&&("a"==a?(r=e[0],l=e[1]):(r=e[1],l=e[0]),t=r.join("
"),n=l.join("
"))}this.spacerHeights=p(i,r,l,x,y.dummy),T=()=>{this.spacerHeights=p(i,r,l,x,y.dummy),d.updateSpacersVars(this.spacerHeights),o&&o(),console.log("resizing")},window.addEventListener("resize",T)}else this.spacerHeights=i(e,t,n,x,y.dummy),T=()=>{this.spacerHeights=i(e,t,n,x,y.dummy),d.updateSpacersVars(this.spacerHeights),console.log("resizing"),o&&o()},window.addEventListener("resize",T);d.updateSpacersVars(this.spacerHeights),d.manageExceptions(this.spacerHeights),f.main.innerHTML=e,f.inner.innerHTML=t,f.outer.innerHTML=n;const l=Math.max(...["main","inner","outer"].map((e=>y[e].el.offsetHeight)));y.el.style.height=`${l}px`,r&&r()}}}})); 2 | -------------------------------------------------------------------------------- /dist/daf-renderer.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.dafRenderer = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | const defaultOptions = { 8 | contentWidth: "600px", 9 | mainWidth: "50%", 10 | padding: { 11 | vertical: "10px", 12 | horizontal: "16px", 13 | }, 14 | innerPadding: "4px", 15 | outerPadding: "4px", 16 | halfway: "50%", 17 | fontFamily: { 18 | inner: "Rashi", 19 | outer: "Rashi", 20 | main: "Vilna" 21 | }, 22 | direction: "rtl", 23 | fontSize: { 24 | main: "15px", 25 | side: "10.5px" 26 | }, 27 | lineHeight: { 28 | main: "17px", 29 | side: "14px", 30 | } 31 | }; 32 | 33 | function mergeAndClone (modified, definitional = defaultOptions) { 34 | const newOptions = {}; 35 | for (const key in definitional) { 36 | if (key in modified) { 37 | const defType = typeof definitional[key]; 38 | if (typeof modified[key] !== defType) { 39 | console.error(`Option ${key} must be of type ${defType}; ${typeof modified[key]} was passed.`); 40 | } 41 | if (defType == "object") { 42 | newOptions[key] = mergeAndClone(modified[key], definitional[key]); 43 | } else { 44 | newOptions[key] = modified[key]; 45 | } 46 | } else { 47 | newOptions[key] = definitional[key]; 48 | } 49 | } 50 | return newOptions; 51 | } 52 | 53 | function getAreaOfText(text, font, fs, width, lh, dummy) { 54 | let testDiv = document.createElement("div"); 55 | testDiv.style.font = String(fs) + "px " + String(font); 56 | testDiv.style.width = String(width) + "px"; //You can remove this, but it may introduce unforseen problems 57 | testDiv.style.lineHeight = String(lh) + "px"; 58 | testDiv.innerHTML = text; 59 | dummy.append(testDiv); 60 | let test_area = Number(testDiv.clientHeight * testDiv.clientWidth); 61 | testDiv.remove(); 62 | return test_area; 63 | } 64 | 65 | function calculateSpacers(mainText, innerText, outerText, options, dummy) { 66 | 67 | const parsedOptions = { 68 | width: parseFloat(options.contentWidth), 69 | padding: { 70 | vertical: parseFloat(options.padding.vertical), 71 | horizontal: parseFloat(options.padding.horizontal) 72 | }, 73 | halfway: 0.01 * parseFloat(options.halfway), 74 | fontFamily: options.fontFamily, 75 | fontSize: { 76 | main: parseFloat(options.fontSize.main), 77 | side: parseFloat(options.fontSize.side), 78 | }, 79 | lineHeight: { 80 | main: parseFloat(options.lineHeight.main), 81 | side: parseFloat(options.lineHeight.side), 82 | }, 83 | mainWidth: 0.01 * parseFloat(options.mainWidth) 84 | }; 85 | 86 | const midWidth = Number(parsedOptions.width * parsedOptions.mainWidth) - 2*parsedOptions.padding.horizontal; //main middle strip 87 | const topWidth = Number(parsedOptions.width * parsedOptions.halfway) - parsedOptions.padding.horizontal; //each commentary top 88 | const sideWidth = Number(parsedOptions.width * (1 - parsedOptions.mainWidth)/2); //each commentary widths, dont include padding, sokeep it constant 89 | 90 | const spacerHeights = { 91 | start: 4.3 * parsedOptions.lineHeight.side, 92 | inner: null, 93 | outer: null, 94 | end: 0, 95 | exception: 0 96 | }; 97 | 98 | // We are accounting for the special case, where you have line breaks: 99 | // if (options.lineBreaks) { 100 | // console.log("Special Case for Line Breaks") 101 | // const main = { 102 | // name: "main", 103 | // text: mainText, 104 | // lineHeight: parsedOptions.lineHeight.main, 105 | // top: 0, 106 | // } 107 | // const outer = { 108 | // name: "outer", 109 | // text: outerText, 110 | // lineHeight: parsedOptions.lineHeight.side, 111 | // top: 4, 112 | // } 113 | // const inner = { 114 | // name: "inner", 115 | // text: innerText, 116 | // lineHeight: parsedOptions.lineHeight.side, 117 | // top: 4, 118 | // } 119 | // 120 | // const texts = [main, outer, inner]; 121 | // texts.forEach(body => body.brCount = (body.text.match(/
/g) || []).length - body.top); 122 | // texts.forEach(body => body.height = (body.brCount * body.lineHeight)); 123 | // texts.forEach(body => body.unadjustedHeight = ((body.brCount + body.top + 1) * body.lineHeight)); 124 | // texts.forEach(body => body.unadjustedHeightAlt = ((body.brCount + body.top) * body.lineHeight)*sideWidth/topWidth); 125 | // const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 126 | // 127 | // const exConst = 2.2 128 | // 129 | // //Checking Exceptions: 130 | // if (inner.unadjustedHeight <= 0 && outer.unadjustedHeight <= 0){ 131 | // console.error("No Commentary"); 132 | // return Error("No Commentary"); 133 | // }; 134 | // if (inner.unadjustedHeightAlt/exConst < spacerHeights.start || outer.unadjustedHeightAlt/exConst < spacerHeights.start) { 135 | // console.log("Exceptions") 136 | // if (inner.unadjustedHeightAlt/exConst <= spacerHeights.start) { 137 | // spacerHeights.inner = inner.unadjustedHeight; 138 | // spacerHeights.outer = outer.height 139 | // return spacerHeights; 140 | // } 141 | // if (outer.unadjustedHeightAlt/exConst <= spacerHeights.start) { 142 | // spacerHeights.outer = outer.unadjustedHeight; 143 | // spacerHeights.inner = inner.height; 144 | // return spacerHeights; 145 | // } 146 | // else { 147 | // return Error("Inner Spacer Error"); 148 | // } 149 | // }; 150 | // //If Double=Wrap 151 | // if (perHeight[0].name === "main"){ 152 | // console.log("Double-Wrap"); 153 | // spacerHeights.inner = main.height; 154 | // spacerHeights.outer = main.height; 155 | // 156 | // const brDifference = perHeight[1].brCount - perHeight[0].brCount; 157 | // spacerHeights.end = brDifference*perHeight[1].lineHeight; 158 | // return spacerHeights; 159 | // } 160 | // 161 | // //If Stairs 162 | // if (perHeight[1].name === "main") { 163 | // console.log("Stairs"); 164 | // spacerHeights[perHeight[0].name] = perHeight[0].height; 165 | // spacerHeights[perHeight[2].name] = main.height; 166 | // return spacerHeights; 167 | // } 168 | // 169 | // //If Double Extend 170 | // console.log("Double-Extend") 171 | // spacerHeights.inner = inner.height + (inner.height/inner.height**2)*inner.lineHeight; 172 | // spacerHeights.outer = outer.height + (inner.height/inner.height**2)*outer.lineHeight; 173 | // return spacerHeights 174 | // } 175 | 176 | 177 | // We could probably put this somewhere else, it was meant to be a place for all the padding corrections, 178 | // but there turned out to only be one 179 | const paddingAreas = { 180 | name: "paddingAreas", 181 | horizontalSide: sideWidth * parsedOptions.padding.vertical, 182 | }; 183 | 184 | 185 | const topArea = (lineHeight) => ((4 * lineHeight * topWidth)); //remove area of the top 4 lines 186 | 187 | 188 | const main = { 189 | name: "main", 190 | width: midWidth, 191 | text: mainText, 192 | lineHeight: parsedOptions.lineHeight.main, 193 | area: getAreaOfText(mainText, parsedOptions.fontFamily.main, parsedOptions.fontSize.main, midWidth, parsedOptions.lineHeight.main, dummy), 194 | length: null, 195 | height: null, 196 | }; 197 | const outer = { 198 | name: "outer", 199 | width: sideWidth, 200 | text: outerText, 201 | lineHeight: parsedOptions.lineHeight.side, 202 | area: getAreaOfText(outerText, parsedOptions.fontFamily.outer, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 203 | - topArea(parsedOptions.lineHeight.side), 204 | length: null, 205 | height: null, 206 | }; 207 | const inner = { 208 | name: "inner", 209 | width: sideWidth, 210 | text: innerText, 211 | lineHeight: parsedOptions.lineHeight.side, 212 | area: 213 | getAreaOfText(innerText, parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 214 | - topArea(parsedOptions.lineHeight.side), 215 | length: null, 216 | height: null, 217 | }; 218 | 219 | const texts = [main, outer, inner]; 220 | texts.forEach(text => text.height = text.area / text.width); 221 | texts.forEach(text => text.unadjustedArea = text.area + topArea(parsedOptions.lineHeight.side)); 222 | texts.forEach(text => text.unadjustedHeight = text.unadjustedArea / text.width); 223 | 224 | const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 225 | 226 | //There are Three Main Types of Case: 227 | //Double-Wrap: The main text being the smallest and commentaries wrapping around it 228 | //Stairs: The main text wrapping around one, but the other wrapping around it 229 | //Double-Extend: The main text wrapping around both commentaries 230 | 231 | //Main Text is Smallest: Double-Wrap 232 | //Main Text being Middle: Stairs 233 | //Main Text Being Largest: Double-Extend 234 | 235 | //First we need to check we have enough commentary to fill the first four lines 236 | if (inner.height <= 0 && outer.height <= 0){ 237 | console.error("No Commentary"); 238 | return Error("No Commentary"); 239 | } 240 | 241 | // This is a case that we have to decice what to do with, when there is not enough commentary on both sides to fill the lines. 242 | if (inner.height <= spacerHeights.start && outer.height <= spacerHeights.start) { 243 | console.error("Not Enough Commentary to Fill Four Lines"); 244 | return Error("Not Enough Commentary"); 245 | } 246 | // We are going to deal with our first edge case when there is either only one commentary 247 | // Or where there is enough of one commentary, but not four lines of the other. 248 | if (inner.unadjustedHeight <= spacerHeights.start || outer.unadjustedHeight <= spacerHeights.start) { 249 | if (inner.unadjustedHeight <= spacerHeights.start) { 250 | spacerHeights.inner = inner.unadjustedHeight; 251 | spacerHeights.outer = (outer.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 252 | spacerHeights.exception = 1; 253 | return spacerHeights; 254 | } 255 | if (outer.unadjustedHeight <= spacerHeights.start) { 256 | spacerHeights.outer = outer.unadjustedHeight; 257 | 258 | spacerHeights.inner = (inner.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 259 | spacerHeights.exception = 2; 260 | return spacerHeights; 261 | } 262 | else { 263 | return Error("Inner Spacer Error"); 264 | } 265 | } 266 | //If Double=Wrap 267 | if (perHeight[0].name === "main"){ 268 | console.log("Double-Wrap"); 269 | spacerHeights.inner = main.area/midWidth; 270 | spacerHeights.outer = spacerHeights.inner; 271 | 272 | const sideArea = spacerHeights.inner * sideWidth + paddingAreas.horizontalSide; 273 | const bottomChunk = perHeight[1].area - sideArea; 274 | const bottomHeight = bottomChunk / topWidth; 275 | spacerHeights.end = bottomHeight; 276 | return spacerHeights; 277 | } 278 | // If Stairs, there's one text at the bottom. We will call it THE stair. 279 | // The remaining two texts form a "block" that we must compare with that bottom text. 280 | const blockArea = (main.area + perHeight[0].area); 281 | const blockWidth = midWidth + sideWidth; 282 | const blockHeight = blockArea / blockWidth; 283 | 284 | const stair = (perHeight[1].name == "main") ? perHeight[2] : perHeight[1]; 285 | const stairHeight = stair.area / stair.width; 286 | 287 | if (blockHeight < stairHeight) { 288 | console.log(`Stairs, ${stair.name} is the stair`); 289 | // This function accounts for extra space that is introduced by padding 290 | const lilArea = (height1, height2, horizPadding) => (horizPadding) * (height1 - height2); 291 | const smallest = perHeight[0]; 292 | spacerHeights[smallest.name] = smallest.height; 293 | spacerHeights[stair.name] = (blockArea - lilArea(blockHeight, spacerHeights[smallest.name], parsedOptions.padding.horizontal)) / blockWidth; 294 | return spacerHeights 295 | } 296 | //If Double Extend 297 | console.log("Double-Extend"); 298 | spacerHeights.inner = inner.height; 299 | spacerHeights.outer = outer.height; 300 | 301 | return spacerHeights 302 | } 303 | 304 | function styleInject(css, ref) { 305 | if ( ref === void 0 ) ref = {}; 306 | var insertAt = ref.insertAt; 307 | 308 | if (!css || typeof document === 'undefined') { return; } 309 | 310 | var head = document.head || document.getElementsByTagName('head')[0]; 311 | var style = document.createElement('style'); 312 | style.type = 'text/css'; 313 | 314 | if (insertAt === 'top') { 315 | if (head.firstChild) { 316 | head.insertBefore(style, head.firstChild); 317 | } else { 318 | head.appendChild(style); 319 | } 320 | } else { 321 | head.appendChild(style); 322 | } 323 | 324 | if (style.styleSheet) { 325 | style.styleSheet.cssText = css; 326 | } else { 327 | style.appendChild(document.createTextNode(css)); 328 | } 329 | } 330 | 331 | var css_248z = "/*Keep this as the first rule in the file*/\n.styles_dafRoot__1QUlM {\n --contentWidth: 0px;\n --padding-horizontal: 0px;\n --padding-vertical: 0px;\n --halfway: 50%;\n\n --fontFamily-inner: \"Rashi\";\n --fontFamily-outer: \"Tosafot\";\n --fontFamily-main: \"Vilna\";\n --direction: \"rtl\";\n\n --fontSize-main: 0px;\n --fontSize-side: 0px;\n\n --lineHeight-main: 0px;\n --lineHeight-side: 0px;\n\n --mainWidth: 0%;\n --mainMargin-start: var(--mainWidth);\n --sidePercent: calc(calc(100% - var(--mainMargin-start)) / 2);\n --remainderPercent: calc(100% - var(--sidePercent));\n\n --innerFloat: left;\n --outerFloat: right;\n\n --spacerHeights-start: 0px;\n --spacerHeights-outer: 0px;\n --spacerHeights-inner: 0px;\n --spacerHeights-end: 0px;\n\n /*Edge Cases*/\n --hasInnerStartGap: 0;\n --hasOuterStartGap: 0;\n --innerStartWidth: 50%;\n --innerPadding: 0px;\n --outerStartWidth: 50%;\n --outerPadding: 0px;\n}\n\n/*Containers*/\n.styles_dafRoot__1QUlM,\n.styles_outer__abXQX,\n.styles_inner__x-amJ,\n.styles_main__BHTRd {\n width: var(--contentWidth);\n pointer-events: none;\n box-sizing: content-box;\n}\n\n.styles_outer__abXQX, .styles_inner__x-amJ, .styles_main__BHTRd {\n position: absolute;\n}\n\n/*Float changes with amud*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n float: var(--innerFloat);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n float: var(--outerFloat);\n}\n\n/*Spacer widths determined by options*/\n.styles_inner__x-amJ .styles_spacer__2T7TS,\n.styles_outer__abXQX .styles_spacer__2T7TS {\n width: var(--halfway);\n}\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n width: var(--remainderPercent);\n}\n\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--contentWidth);\n}\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n width: var(--sidePercent);\n}\n\n/*Spacer heights determined by algorithm*/\n.styles_spacer__2T7TS.styles_start__AwkfY {\n height: var(--spacerHeights-start);\n}\n\n.styles_spacer__2T7TS.styles_end__2wr6A {\n height: var(--spacerHeights-end);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi {\n height: var(--spacerHeights-inner);\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n height: var(--spacerHeights-outer);\n}\n\n/*Settings to handle edge Cases*/\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--innerStartWidth);\n margin-left: var(--innerPadding);\n margin-right: var(--innerPadding);\n}\n\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n width: var(--outerStartWidth);\n margin-left: var(--outerPadding);\n margin-right: var(--outerPadding);\n}\n\n.styles_inner__x-amJ .styles_spacer__2T7TS.styles_start__AwkfY{\n margin-bottom: calc(var(--padding-vertical) * var(--hasInnerStartGap));\n}\n.styles_outer__abXQX .styles_spacer__2T7TS.styles_start__AwkfY {\n margin-bottom: calc(var(--padding-vertical) * var(--hasOuterStartGap));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr {\n clear: both;\n}\n\n/*Margins!*/\n.styles_spacer__2T7TS.styles_start__AwkfY,\n.styles_spacer__2T7TS.styles_end__2wr6A,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-left: calc(0.5 * var(--padding-horizontal));\n margin-right: calc(0.5 * var(--padding-horizontal));\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_text__1_7-z {\n margin-top: var(--padding-vertical);\n}\n\n.styles_spacer__2T7TS.styles_mid__dcgUr,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_innerMid__27MCi,\n.styles_main__BHTRd .styles_spacer__2T7TS.styles_outerMid__2WtcY {\n margin-bottom: var(--padding-vertical);\n}\n\n/*Text*/\n.styles_text__1_7-z {\n direction: var(--direction);\n text-align: justify;\n text-align-last: center;\n}\n\n.styles_text__1_7-z span {\n pointer-events: auto;\n}\n\n.styles_main__BHTRd .styles_text__1_7-z {\n font-family: var(--fontFamily-main);\n font-size: var(--fontSize-main);\n line-height: var(--lineHeight-main);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z,\n.styles_outer__abXQX .styles_text__1_7-z {\n font-size: var(--fontSize-side);\n line-height: var(--lineHeight-side);\n}\n\n.styles_inner__x-amJ .styles_text__1_7-z {\n font-family: var(--fontFamily-inner);\n}\n\n.styles_outer__abXQX .styles_text__1_7-z {\n font-family: var(--fontFamily-outer);\n}\n"; 332 | var classes = {"dafRoot":"styles_dafRoot__1QUlM","outer":"styles_outer__abXQX","inner":"styles_inner__x-amJ","main":"styles_main__BHTRd","spacer":"styles_spacer__2T7TS","outerMid":"styles_outerMid__2WtcY","innerMid":"styles_innerMid__27MCi","mid":"styles_mid__dcgUr","start":"styles_start__AwkfY","end":"styles_end__2wr6A","text":"styles_text__1_7-z"}; 333 | styleInject(css_248z); 334 | 335 | const sideSpacersClasses = { 336 | start: [classes.spacer, classes.start], 337 | mid: [classes.spacer, classes.mid], 338 | end: [classes.spacer, classes.end] 339 | }; 340 | 341 | const containerClasses = { 342 | el: classes.dafRoot, 343 | outer: { 344 | el: classes.outer, 345 | spacers: sideSpacersClasses, 346 | text: classes.text, 347 | }, 348 | inner: { 349 | el: classes.inner, 350 | spacers: sideSpacersClasses, 351 | text: classes.text, 352 | }, 353 | main: { 354 | el: classes.main, 355 | spacers: { 356 | start: sideSpacersClasses.start, 357 | inner: [classes.spacer, classes.innerMid], 358 | outer: [classes.spacer, classes.outerMid] 359 | }, 360 | text: classes.text 361 | } 362 | }; 363 | 364 | function addClasses(element, classNames) { 365 | if (Array.isArray(classNames)) 366 | element.classList.add(...classNames); 367 | else 368 | element.classList.add(classNames); 369 | } 370 | 371 | function setVars(object, prefix = "") { 372 | const varsRule = Array.prototype.find.call 373 | (document.styleSheets, sheet => sheet.rules[0].selectorText == `.${classes.dafRoot}`) 374 | .rules[0]; 375 | 376 | Object.entries(object).forEach(([key, value]) => { 377 | if (typeof value == "string") { 378 | varsRule.style.setProperty(`--${prefix}${key}`, value); 379 | } else if (typeof value == "object") { 380 | setVars(value, `${key}-`); 381 | } 382 | }); 383 | } 384 | 385 | 386 | let appliedOptions; 387 | var styleManager = { 388 | applyClasses(containers, classesMap = containerClasses) { 389 | for (const key in containers) { 390 | if (key in classesMap) { 391 | const value = classesMap[key]; 392 | if (typeof value === "object" && !Array.isArray(value)) { 393 | this.applyClasses(containers[key], value); 394 | } else { 395 | addClasses(containers[key], value); 396 | } 397 | } 398 | } 399 | }, 400 | updateOptionsVars(options) { 401 | appliedOptions = options; 402 | setVars(options); 403 | }, 404 | updateSpacersVars(spacerHeights) { 405 | setVars( 406 | Object.fromEntries( 407 | Object.entries(spacerHeights).map( 408 | ([key, value]) => ([key, String(value) + 'px'])) 409 | ), 410 | "spacerHeights-" 411 | ); 412 | }, 413 | updateIsAmudB(amudB) { 414 | setVars({ 415 | innerFloat: amudB ? "right" : "left", 416 | outerFloat: amudB ? "left" : "right" 417 | }); 418 | }, 419 | manageExceptions(spacerHeights) { 420 | if (!spacerHeights.exception) { 421 | setVars({ 422 | hasOuterStartGap: "0", 423 | hasInnerStartGap: "0", 424 | outerStartWidth: "50%", 425 | innerStartWidth: "50%", 426 | innerPadding: appliedOptions.innerPadding, 427 | outerPadding: appliedOptions.outerPadding, 428 | }); 429 | return; 430 | } 431 | if (spacerHeights.exception === 1) { 432 | console.log("In Style Exception, Case 1"); 433 | setVars({ 434 | hasInnerStartGap: "1", 435 | innerStartWidth: "100%", 436 | outerStartWidth: "0%", 437 | innerPadding: "0px", 438 | outerPadding: "0px", 439 | }); 440 | } else if (spacerHeights.exception === 2) { 441 | console.log("In Style Exception, Case 2"); 442 | setVars({ 443 | hasOuterStartGap: "1", 444 | outerStartWidth: "100%", 445 | innerStartWidth: "0%", 446 | innerPadding: "0px", 447 | outerPadding: "0px" 448 | }); 449 | } 450 | } 451 | }; 452 | 453 | function getLineInfo(text, font, fontSize, lineHeight, dummy) { 454 | dummy.innerHTML = ""; 455 | let testDiv = document.createElement("span"); 456 | testDiv.style.font = fontSize + " " + String(font); 457 | testDiv.style.lineHeight = String(lineHeight) + "px"; 458 | testDiv.innerHTML = text; 459 | testDiv.style.position = "absolute"; 460 | dummy.append(testDiv); 461 | const rect = testDiv.getBoundingClientRect(); 462 | const height = rect.height; 463 | const width = rect.width; 464 | const widthProportional = width / dummy.getBoundingClientRect().width; 465 | testDiv.remove(); 466 | return {height, width, widthProportional}; 467 | } 468 | 469 | function heightAccumulator(font, fontSize, lineHeight, dummy) { 470 | return (lines) => { 471 | return getLineInfo(lines.join("
"), font, fontSize, lineHeight, dummy).height; 472 | } 473 | } 474 | 475 | function getBreaks(sizeArray) { 476 | const widths = sizeArray.map(size => size.widthProportional); 477 | const diffs = widths.map((width, index, widths) => index == 0 ? 0 : Math.abs(width - widths[index - 1])); 478 | const threshold = 0.12; 479 | let criticalPoints = diffs.reduce((indices, curr, currIndex) => { 480 | //Breaks before line 4 are flukes 481 | if (currIndex < 4) return indices; 482 | if (curr > threshold) { 483 | //There should never be two breakpoints in a row 484 | const prevIndex = indices[indices.length - 1]; 485 | if (prevIndex && (currIndex - prevIndex) == 1) { 486 | return indices; 487 | } 488 | indices.push(currIndex); 489 | } 490 | return indices; 491 | }, []); 492 | const averageAround = points => points.map((point, i) => { 493 | let nextPoint; 494 | if (!nextPoint) { 495 | nextPoint = Math.min(point + 3, widths.length - 1); 496 | } 497 | let prevPoint; 498 | if (!prevPoint) { 499 | prevPoint = Math.max(point - 3, 0); 500 | } 501 | /* 502 | Note that these are divided by the width of the critical point line such that 503 | we get the average width of the preceeding and proceeding chunks *relative* 504 | to the critical line. 505 | */ 506 | const before = (widths.slice(prevPoint, point).reduce((acc, curr) => acc + curr) / 507 | (point - prevPoint)) / widths[point]; 508 | let after; 509 | if ( point + 1 >= nextPoint) { 510 | after = widths[nextPoint] / widths[point]; 511 | } else { 512 | after =(widths.slice(point + 1, nextPoint).reduce((acc, curr) => acc + curr) / 513 | (nextPoint - point - 1)) / widths[point]; 514 | } 515 | return { 516 | point, 517 | before, 518 | after, 519 | diff: Math.abs(after - before) 520 | } 521 | }); 522 | const aroundDiffs = averageAround(criticalPoints) 523 | .sort( (a,b) => b.diff - a.diff); 524 | criticalPoints = aroundDiffs 525 | .filter( ({diff}) => diff > 0.22) 526 | .map( ({point}) => point); 527 | return criticalPoints.sort( (a, b) => a - b); 528 | } 529 | 530 | function onlyOneCommentary(lines, options, dummy) { 531 | const fontFamily = options.fontFamily.inner; 532 | const fontSize = options.fontSize.side; 533 | const lineHeight = parseFloat(options.lineHeight.side); 534 | const sizes = lines.map(text => getLineInfo(text, fontFamily, fontSize, lineHeight, dummy)); 535 | const breaks = getBreaks(sizes); 536 | if (breaks.length == 3) { 537 | const first = lines.slice(0, breaks[1]); 538 | const second = lines.slice(breaks[1]); 539 | return [first, second]; 540 | } 541 | } 542 | 543 | function calculateSpacersBreaks(mainArray, rashiArray, tosafotArray, options, dummy) { 544 | const lines = { 545 | main: mainArray, 546 | rashi: rashiArray, 547 | tosafot: tosafotArray 548 | }; 549 | 550 | const parsedOptions = { 551 | padding: { 552 | vertical: parseFloat(options.padding.vertical), 553 | horizontal: parseFloat(options.padding.horizontal) 554 | }, 555 | halfway: 0.01 * parseFloat(options.halfway), 556 | fontFamily: options.fontFamily, // Object of strings 557 | fontSize: { 558 | main: options.fontSize.main, 559 | side: options.fontSize.side, 560 | }, 561 | lineHeight: { 562 | main: parseFloat(options.lineHeight.main), 563 | side: parseFloat(options.lineHeight.side), 564 | }, 565 | }; 566 | 567 | 568 | const mainOptions = [parsedOptions.fontFamily.main, parsedOptions.fontSize.main, parsedOptions.lineHeight.main]; 569 | const commentaryOptions = [parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, parsedOptions.lineHeight.side]; 570 | 571 | const sizes = {}; 572 | sizes.main = lines.main.map(text => getLineInfo(text, ...mainOptions, dummy)); 573 | ["rashi", "tosafot"].forEach(text => { 574 | sizes[text] = lines[text].map(line => getLineInfo(line, ...commentaryOptions, dummy)); 575 | }); 576 | 577 | const accumulateMain = heightAccumulator(...mainOptions, dummy); 578 | const accumulateCommentary = heightAccumulator(...commentaryOptions, dummy); 579 | 580 | const breaks = {}; 581 | 582 | ["rashi", "tosafot", "main"].forEach(text => { 583 | breaks[text] = getBreaks(sizes[text]) 584 | /* 585 | Hadran lines aren't real candidates for line breaks. 586 | TODO: Extract this behavior , give it an option/parameter 587 | */ 588 | .filter(lineNum => !(lines[text][lineNum].includes("hadran")) 589 | ); 590 | }); 591 | 592 | 593 | const spacerHeights = { 594 | start: 4.4 * parsedOptions.lineHeight.side, 595 | inner: null, 596 | outer: null, 597 | end: 0, 598 | exception: 0 599 | }; 600 | 601 | const mainHeight = accumulateMain(lines.main); 602 | const mainHeightOld = (sizes.main.length) * parsedOptions.lineHeight.main; 603 | let afterBreak = { 604 | inner: accumulateCommentary(lines.rashi.slice(4)), 605 | outer: accumulateCommentary(lines.tosafot.slice(4)) 606 | }; 607 | 608 | let afterBreakOld = { 609 | inner: parsedOptions.lineHeight.side * (sizes.rashi.length - 4), 610 | outer: parsedOptions.lineHeight.side * (sizes.tosafot.length - 4) 611 | }; 612 | 613 | if (breaks.rashi.length < 1 || breaks.tosafot.length < 1) { 614 | console.log("Dealing with Exceptions"); 615 | if (breaks.rashi.length < 1) { 616 | afterBreak.inner = parsedOptions.lineHeight.side * (sizes.rashi.length + 1); 617 | spacerHeights.exception = 2; 618 | } 619 | if (breaks.tosafot.length < 1) { 620 | afterBreak.outer = parsedOptions.lineHeight.side * (sizes.tosafot.length + 1); 621 | spacerHeights.exception = 2; 622 | } 623 | } 624 | switch (breaks.main.length) { 625 | case 0: 626 | spacerHeights.inner = mainHeight; 627 | spacerHeights.outer = mainHeight; 628 | if (breaks.rashi.length == 2) { 629 | spacerHeights.end = accumulateCommentary(lines.rashi.slice(breaks.rashi[1])); 630 | } else { 631 | spacerHeights.end = accumulateCommentary(lines.tosafot.slice(breaks.tosafot[1])); 632 | } 633 | console.log("Double wrap"); 634 | break; 635 | case 1: 636 | if (breaks.rashi.length != breaks.tosafot.length) { 637 | if (breaks.tosafot.length == 0) { 638 | spacerHeights.outer = 0; 639 | spacerHeights.inner = afterBreak.inner; 640 | break; 641 | } 642 | if (breaks.rashi.length == 0) { 643 | spacerHeights.inner = 0; 644 | spacerHeights.outer = afterBreak.outer; 645 | break; 646 | } 647 | let stair; 648 | let nonstair; 649 | if (breaks.rashi.length == 1) { 650 | stair = "outer"; 651 | nonstair = "inner"; 652 | } else { 653 | stair = "inner"; 654 | nonstair = "outer"; 655 | } 656 | spacerHeights[nonstair] = afterBreak[nonstair]; 657 | spacerHeights[stair] = mainHeight; 658 | console.log("Stairs"); 659 | break; 660 | } 661 | case 2: 662 | spacerHeights.inner = afterBreak.inner; 663 | spacerHeights.outer = afterBreak.outer; 664 | console.log("Double Extend"); 665 | break; 666 | default: 667 | spacerHeights.inner = afterBreak.inner; 668 | spacerHeights.outer = afterBreak.outer; 669 | console.log("No Case Exception"); 670 | break; 671 | } 672 | return spacerHeights; 673 | } 674 | 675 | function el(tag, parent) { 676 | const newEl = document.createElement(tag); 677 | if (parent) parent.append(newEl); 678 | return newEl; 679 | } 680 | 681 | function div(parent) { 682 | return el("div", parent); 683 | } 684 | 685 | function span(parent) { 686 | return el("span", parent); 687 | } 688 | 689 | 690 | function renderer (el, options = defaultOptions) { 691 | const root = (typeof el === "string") ? document.querySelector(el) : el; 692 | if (!(root && root instanceof Element && root.tagName.toUpperCase() === "DIV")) { 693 | throw "Argument must be a div element or its selector" 694 | } 695 | const outerContainer = div(root); 696 | const innerContainer = div(root); 697 | const mainContainer = div(root); 698 | const dummy = div(root); 699 | dummy.id = "dummy"; 700 | const containers = { 701 | el: root, 702 | dummy: dummy, 703 | outer: { 704 | el: outerContainer, 705 | spacers: { 706 | start: div(outerContainer), 707 | mid: div(outerContainer), 708 | end: div(outerContainer) 709 | }, 710 | text: div(outerContainer) 711 | }, 712 | inner: { 713 | el: innerContainer, 714 | spacers: { 715 | start: div(innerContainer), 716 | mid: div(innerContainer), 717 | end: div(innerContainer) 718 | }, 719 | text: div(innerContainer) 720 | }, 721 | main: { 722 | el: mainContainer, 723 | spacers: { 724 | start: div(mainContainer), 725 | inner: div(mainContainer), 726 | outer: div(mainContainer), 727 | }, 728 | text: div(mainContainer) 729 | } 730 | }; 731 | 732 | const textSpans = { 733 | main: span(containers.main.text), 734 | inner: span(containers.inner.text), 735 | outer: span(containers.outer.text) 736 | }; 737 | 738 | const clonedOptions = mergeAndClone(options, defaultOptions); 739 | 740 | styleManager.applyClasses(containers); 741 | styleManager.updateOptionsVars(clonedOptions); 742 | 743 | let resizeEvent; 744 | return { 745 | classes, 746 | containers, 747 | spacerHeights: { 748 | start: 0, 749 | inner: 0, 750 | outer: 0, 751 | end: 0 752 | }, 753 | amud: "a", 754 | render(main, inner, outer, amud = "a", linebreak, renderCallback, resizeCallback) { 755 | if (resizeEvent) { 756 | window.removeEventListener("resize", resizeEvent); 757 | } 758 | if (this.amud != amud) { 759 | this.amud = amud; 760 | styleManager.updateIsAmudB(amud == "b"); 761 | } 762 | if (!linebreak) { 763 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 764 | resizeEvent = () => { 765 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 766 | styleManager.updateSpacersVars(this.spacerHeights); 767 | console.log("resizing"); 768 | if (resizeCallback) 769 | resizeCallback(); 770 | }; 771 | window.addEventListener("resize", resizeEvent); 772 | } 773 | else { 774 | let [mainSplit, innerSplit, outerSplit] = [main, inner, outer].map( text => { 775 | containers.dummy.innerHTML = text; 776 | const divRanges = Array.from(containers.dummy.querySelectorAll("div")).map(div => { 777 | const range = document.createRange(); 778 | range.selectNode(div); 779 | return range; 780 | }); 781 | 782 | const brs = containers.dummy.querySelectorAll(linebreak); 783 | const splitFragments = []; 784 | brs.forEach((node, index) => { 785 | const range = document.createRange(); 786 | range.setEndBefore(node); 787 | if (index == 0) { 788 | range.setStart(containers.dummy, 0); 789 | } else { 790 | const prev = brs[index - 1]; 791 | range.setStartAfter(prev); 792 | } 793 | divRanges.forEach( (divRange, i) => { 794 | const inBetween = range.compareBoundaryPoints(Range.START_TO_START, divRange) < 0 && range.compareBoundaryPoints(Range.END_TO_END, divRange) > 0; 795 | if (inBetween) { 796 | splitFragments.push(divRange.extractContents()); 797 | divRanges.splice(i, 1); 798 | } 799 | }); 800 | 801 | splitFragments.push(range.extractContents()); 802 | }); 803 | 804 | return splitFragments.map(fragment => { 805 | const el = document.createElement("div"); 806 | el.append(fragment); 807 | return el.innerHTML; 808 | }) 809 | }); 810 | 811 | containers.dummy.innerHTML = ""; 812 | 813 | const hasInner = innerSplit.length != 0; 814 | const hasOuter = outerSplit.length != 0; 815 | 816 | if (hasInner != hasOuter) { 817 | const withText = hasInner ? innerSplit : outerSplit; 818 | const fixed = onlyOneCommentary(withText, clonedOptions, dummy); 819 | if (fixed) { 820 | if (amud == "a") { 821 | innerSplit = fixed[0]; 822 | outerSplit = fixed[1]; 823 | } else { 824 | innerSplit = fixed[1]; 825 | outerSplit = fixed[0]; 826 | } 827 | inner = innerSplit.join('
'); 828 | outer = outerSplit.join('
'); 829 | } 830 | } 831 | 832 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 833 | resizeEvent = () => { 834 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 835 | styleManager.updateSpacersVars(this.spacerHeights); 836 | if (resizeCallback) 837 | resizeCallback(); 838 | console.log("resizing"); 839 | }; 840 | window.addEventListener('resize', resizeEvent); 841 | } 842 | styleManager.updateSpacersVars(this.spacerHeights); 843 | styleManager.manageExceptions(this.spacerHeights); 844 | textSpans.main.innerHTML = main; 845 | textSpans.inner.innerHTML = inner; 846 | textSpans.outer.innerHTML = outer; 847 | 848 | const containerHeight = Math.max(...["main", "inner", "outer"].map(t => containers[t].el.offsetHeight)); 849 | containers.el.style.height = `${containerHeight}px`; 850 | if (renderCallback) 851 | renderCallback(); 852 | }, 853 | } 854 | } 855 | 856 | return renderer; 857 | 858 | }))); 859 | -------------------------------------------------------------------------------- /doc-pictures/Adding Areas.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Adding Areas.PNG -------------------------------------------------------------------------------- /doc-pictures/Area Labels.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Area Labels.PNG -------------------------------------------------------------------------------- /doc-pictures/Comparing Areas.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Comparing Areas.PNG -------------------------------------------------------------------------------- /doc-pictures/Double-Extend Case.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Double-Extend Case.PNG -------------------------------------------------------------------------------- /doc-pictures/Double-Wrap Case.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Double-Wrap Case.PNG -------------------------------------------------------------------------------- /doc-pictures/Spacer Labels.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Spacer Labels.PNG -------------------------------------------------------------------------------- /doc-pictures/Spacers Together.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Spacers Together.PNG -------------------------------------------------------------------------------- /doc-pictures/Spacers.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Spacers.PNG -------------------------------------------------------------------------------- /doc-pictures/Stairs Case.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Stairs Case.PNG -------------------------------------------------------------------------------- /doc-pictures/Three Cases.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/doc-pictures/Three Cases.PNG -------------------------------------------------------------------------------- /examples/edgeCase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Daf Renderer Example 2 7 | 8 | 9 |
10 | 11 | 12 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/example2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Daf Renderer Example 2 7 | 8 | 9 |
10 | 11 | 12 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/fonts/Mekorot-Rashi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/examples/fonts/Mekorot-Rashi.ttf -------------------------------------------------------------------------------- /examples/fonts/Mekorot-Vilna-Bold-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/examples/fonts/Mekorot-Vilna-Bold-Italic.ttf -------------------------------------------------------------------------------- /examples/fonts/Mekorot-Vilna-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/examples/fonts/Mekorot-Vilna-Bold.ttf -------------------------------------------------------------------------------- /examples/fonts/Mekorot-Vilna-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/examples/fonts/Mekorot-Vilna-Italic.ttf -------------------------------------------------------------------------------- /examples/fonts/Mekorot-Vilna.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalmudLab/daf-renderer/b2818adfd245e4a4857ad941d1d76e6e2eb42c62/examples/fonts/Mekorot-Vilna.ttf -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------*/ 2 | // THIS FILE IS NOT MEANT TO BE INCLUDED IN THE FINAL PACKAGE 3 | // IT IS A REMEMNANT OF EARLIER DEVELOPMENT FOR CALLING THE SEFARIA API 4 | // DO NOT PAY IT ANY ATTENTION 5 | // EVENTUALLY WE WILL TURN THIS INTO A CLEAR EXAMPLE OF HOW TO CALL THE SEFARIA API 6 | /*-------------------------------------------------------*/ 7 | 8 | 9 | const texts = { main: [], rashi: [], tosafot: [], sefariaTranslations: []}; 10 | const spans = { main: [], rashi: [], tosafot: []}; 11 | const sentenceSpans = { main: [] } 12 | const charIndexToSpan = { main: [], rashi: [], tosafot: []} // set when getSpanByCharIndex is first called 13 | const spanPrefixes = { main: 'word-main', rashi: 'word-rashi', tosafot: 'word-tosafot', sentence: 'sentence-main' }; 14 | 15 | let tractate = 'Brachot'; 16 | let daf = 42; //Set What daf you want 17 | let amud = 'b'; // Set what amud you want, a or bd 18 | let hebrewRef = ''; 19 | let next = []; //tractate, daf, amud; e.g., ['Eruvin', '13', a'] 20 | let prev = []; //tractate, daf, amud 21 | 22 | 23 | window.addEventListener("load", () => { 24 | getTexts(tractate, daf, amud); 25 | }) 26 | function getTexts(tractate, daf, amud) { 27 | //For now this is how we are accessing Texts 28 | commentary = checkRashbam(tractate, daf, amud); 29 | 30 | const uri1 = 'https://www.sefaria.org/api/texts/' + tractate + '.' + daf + amud + '?' + 'vhe=Wikisource_Talmud_Bavli', 31 | uri2 = 'https://www.sefaria.org/api/texts/' + commentary +'_on_' + tractate + '.' + daf + amud + '.1-100' + '?', 32 | uri3 = 'https://www.sefaria.org/api/texts/Tosafot_on_' + tractate + '.' + daf + amud + '.1-100' + '?'; 33 | 34 | const renderer = dafRenderer("#daf", { padding: { vertical: "10px"}}); 35 | 36 | Promise.all([jsonp(uri1), jsonp(uri2), jsonp(uri3)]).then(values => { 37 | 38 | const main = values[0]; 39 | const rashi = values[1]; 40 | const tosafot = values[2]; 41 | 42 | //Set next and prev for the arrow buttons. 43 | //Sefaria does give you next and prev... sometimes. So we're not relying on that. 44 | // const tractateDetails = tractates[tractate]; 45 | // if (daf + amud == tractateDetails.lastPage) { 46 | // next = [tractateDetails.nextTractate, 2, a] 47 | // } 48 | // else { 49 | // next = (amud == 'a') ? [tractate, daf, 'b'] : [tractate, daf + 1, 'a']; 50 | // } 51 | // if (daf + amud == '2a') { 52 | // const lastPage = tractates[tractateDetails.prevTractate].lastPage; 53 | // const lastPageAmud = lastPage.slice(-1); 54 | // const lastPageDaf = lastPage.slice(0, -1) 55 | // prev = [tractateDetails.prevTractate, lastPageDaf, lastPageAmud]; 56 | // } 57 | // else { 58 | // prev = (amud == 'a') ? [tractate, daf - 1, 'b'] : [tractate, daf, 'a']; 59 | // } 60 | // next = main.next.split(/ (?=\d)|(?=[ab])/g); 61 | // prev = main.prev.split(/ (?=\d)|(?=[ab])/g); 62 | 63 | heRef = main.heRef; 64 | 65 | heRefArray = main.heRef.replace('״', '').split(' '); 66 | if (heRefArray.length == 4) { 67 | heRefArray[0] += ' ' + heRefArray[1]; 68 | heRefArray.splice(1,1); 69 | } 70 | 71 | // setTitle(heRefArray[0], tractate, heRefArray[1], daf, heRefArray[2] == 'א'); 72 | 73 | 74 | texts.rashi = reformatRashi(rashi.he); 75 | texts.tosafot = reformatTosafot(tosafot.he); 76 | texts.main = reformatMain(main.he) 77 | texts.sefariaTranslations = main.text; 78 | 79 | sentenceSpans.main = texts.main.map( 80 | (sentence, index) => { return { sentence: sentence, id: `${spanPrefixes.sentence}-${index}`}}) 81 | 82 | //This code works, and I'm sure it could use some refactoring 83 | //(It's the word-span-wrapping function, but now it has to not step on the toes of the bolding functionality) 84 | const processText = (text, prefix, indexStart = 0) => text 85 | .split(/(\<[^<>]+\>[^<>]+<[^<>]+>)/g) //Break out all HTML elements 86 | .join('***') //Join using so we can do another split 87 | .split(/ (?![^<]*>|<)/g) //Break out all spaces that are not preceeding tags or within tag declarations 88 | .join('***') //Join so we're back to one string, with everything separated by commas 89 | .split('***') //Split into a final, one-dimensional array of words 90 | .filter(Boolean) //Get rid of empty elements 91 | .map(n => n.includes('img') ? wrapImage(n) : n) //Wrap image elements with hover buttons that show and hide them 92 | .map( (word, index) => { return {word: word, id: `${prefix}-${index + indexStart}`} }); 93 | 94 | let nthVerse = 0; 95 | const cutVerses = string => { 96 | const verse = /\(([^\(\)]*,[^\(\)]*)\)/g; 97 | return string.replace(verse, (match, verseRef) => verseLinkSpan(verseRef, ++nthVerse)); 98 | } 99 | 100 | const wordToHTML = spanObj => wordSpan(spanObj.id, spanObj.word); 101 | const sentenceToHTML = spanObj => sentenceSpan(spanObj.id, spanObj.sentence); 102 | 103 | sentenceSpans.main = sentenceSpans.main.map( (sentenceObj) => { 104 | const wordSpanObj = 105 | processText( 106 | cutVerses(sentenceObj.sentence), 107 | spanPrefixes.main, 108 | spans.main.length 109 | ) 110 | .map(wordSpanObj => Object.assign(wordSpanObj, {sentence: sentenceObj.id})); 111 | spans.main = spans.main.concat(wordSpanObj); //OY A SIDE EFFECT (refactor? though this is more efficient than looping twice) 112 | return { 113 | id: sentenceObj.id, 114 | sentence: wordSpanObj.map(wordToHTML).join(' ') 115 | } 116 | }) 117 | 118 | spans.rashi = processText(texts.rashi, spanPrefixes.rashi); 119 | spans.tosafot = processText(texts.tosafot, spanPrefixes.tosafot); 120 | 121 | const mainHTML = sentenceSpans.main.map(sentenceToHTML).join(' '); 122 | const rashiHTML = spans.rashi.map(wordToHTML).join(' ') 123 | const tosafotHTML = spans.tosafot.map(wordToHTML).join(' ') 124 | 125 | console.log("Main:", mainHTML) 126 | console.log("Rashi:", rashiHTML) 127 | console.log("Tosafot:", tosafotHTML) 128 | console.log("Amud:", amud) 129 | 130 | 131 | // document.getElementById('maintxtcntr').innerHTML = "" + mainHTML + ""; 132 | // document.getElementById('innertxtcntr').innerHTML = "" + rashiHTML + ""; 133 | // document.getElementById('outertxtcntr').innerHTML = "" + tosafotHTML + "";; 134 | 135 | setTimeout(function(){ 136 | renderer.render(mainHTML, rashiHTML, tosafotHTML, amud); 137 | // document.querySelector('.daf-container').style.opacity = '1'; 138 | // changePadding(); 139 | 140 | // Should probably be its own function, but need to do it after all of this and couldnt figure out how 141 | // spaceLastLine('maintxtcontent'); 142 | }, 10) 143 | 144 | }) 145 | 146 | } 147 | 148 | 149 | //TITLE CONTROLS 150 | // document.querySelector('.daf-button-prev').addEventListener('click', prevPage); 151 | // document.querySelector('.daf-button-next').addEventListener('click', nextPage); 152 | // 153 | // document.querySelector('.daf-title-tractate-dropdown').addEventListener('change', event => { 154 | // tractate = event.target.value; 155 | // getTexts(tractate, daf, amud); 156 | // }); 157 | // 158 | // document.querySelector('.daf-page-dropdown').addEventListener('change', event => { 159 | // amud = event.target.value.slice(-1); 160 | // daf = event.target.value.slice(0, -1); 161 | // getTexts(tractate, daf, amud); 162 | // }); 163 | // 164 | // function nextPage() { 165 | // getTexts(...next); 166 | // } 167 | // 168 | // function prevPage() { 169 | // getTexts(...prev); 170 | // } 171 | // 172 | // function setTitle(mesekhet, tractate, daf, page, amudAlef = true) { 173 | // document.querySelector('.daf-title-tractate-hebrew').innerText = mesekhet; 174 | // document.querySelector('.daf-title-page').innerText = (amudAlef ? '.' : ':') + daf ; 175 | // document.querySelector('.daf-title-tractate-dropdown').value = tractate; 176 | // 177 | // const dafSelection = document.querySelector('.daf-page-dropdown'); 178 | // const currTractateLength = tractates[tractate].length; 179 | // 180 | // if (dafSelection.options.length < currTractateLength) { 181 | // for (let i = dafSelection.options.length; i < currTractateLength; i++){ 182 | // const amudA = i % 2 == 0; 183 | // const daf = 2 + (i - (i % 2)) / 2; 184 | // const dafString = daf + (amudA ? 'a' : 'b'); 185 | // const opt = document.createElement('option'); 186 | // opt.value = dafString; 187 | // opt.innerHTML = dafString; 188 | // dafSelection.appendChild(opt); 189 | // } 190 | // } else { 191 | // //dafSelection.options and its indexes will change as we delete items 192 | // const difference = dafSelection.options.length - currTractateLength; 193 | // for (let i = 0; i < difference; i++) { 194 | // dafSelection.options.remove(currTractateLength); 195 | // } 196 | // } 197 | // 198 | // document.querySelector('.daf-page-dropdown').value = page + (amudAlef ? 'a' : 'b'); 199 | // } 200 | 201 | // function getHorizontalPos(el) { 202 | // const wdth = document.getElementsByClassName('maintxtcontent')[0].offsetWidth; 203 | // for (var lx_left=0, lx_right=0; 204 | // el != null; 205 | // lx_left += el.offsetLeft, lx_right = wdth - lx_left, el = el.offsetParent); 206 | // return {left: lx_left, right: lx_right}; 207 | // } 208 | //TEXT PROCESSING AND HTML GENERATION 209 | const wordSpan = (id, word) => `${word}` 210 | 211 | const sentenceSpan = (id, sentence) => `${sentence}` 212 | 213 | const verseLinkSpan = (verse, number) => 214 | "${number}` 217 | 218 | let nthImage = 0; 219 | const wrapImage = imageEl => 220 | ` 226 | 227 | ${imageEl} 228 | ` 229 | const wrapSentences = sentenceArray => sentenceArray.map( 230 | (sentence, index) => sentenceSpan(sentence, `${spanPrefixes.sentence}-${index}`)) 231 | 232 | const reformatMain = text => 233 | text.map(string => string 234 | .replace(/:,/g, ": ") 235 | .replace(//g, "") 236 | .replace(/<\/strong>/g, "")) 237 | 238 | const reformatRashi = text => 239 | text.filter(arr => arr.length > 0) 240 | .map(subarr => subarr.map(str => 241 | str.replace(/([^-:]*) - ([^-:]+:)/, ("$1. $2 ")) 242 | ).join('')) 243 | .join('') 244 | .replace(/,,/g, "") 245 | .replace(/,:/g, ": ") 246 | .replace(/:,/g, ": ") 247 | 248 | const reformatTosafot = text => 249 | text.filter(arr => arr.length > 0) 250 | .map(subarr => subarr.map(str => 251 | str.replace(/([^-:]*) - ([^-:]+:)/, ("$1. $2 ")) 252 | ).join('')) 253 | .join('') 254 | .replace(/,,/g, "") 255 | .replace(/,:/g, ": ") 256 | .replace(/:,/g, ": ") 257 | 258 | //SPAN ACCESS 259 | // function getSpanByWordIndex(text, index) { 260 | // return spans[text][index]; 261 | // } 262 | // 263 | // function getSpanByWordId(text, id) { 264 | // const index = id.match(/\d+/)[0]; 265 | // return spans[text][index]; 266 | // } 267 | // 268 | // function getSpanByCharIndex(text, index) { 269 | // if (charIndexToSpan[text].length) { 270 | // return charIndexToSpan[text][index]; 271 | // } 272 | // else { 273 | // let spaceCount = 0; 274 | // let returnVal; 275 | // for (let i = 0; i < texts[text].length; i++) { 276 | // if (texts[text][i] === ' ') { 277 | // spaceCount++; 278 | // } 279 | // const span = spans[text][spaceCount]; 280 | // charIndexToSpan[text][i] = span; 281 | // if (i === index) { 282 | // returnVal = span; 283 | // } 284 | // } 285 | // return returnVal; 286 | // } 287 | // 288 | // } 289 | // 290 | // function getSpansBySearch(text, query) { 291 | // const regex = new RegExp(query, 'gi'); 292 | // let spans = [], result; 293 | // while ( (result = regex.exec(texts[text])) ) { 294 | // spans.push(getSpanByCharIndex(text, result.index)); 295 | // } 296 | // return spans; 297 | // } 298 | // 299 | // function getSefariaTranslation(sentenceIndex) { 300 | // return texts.sefariaTranslations[sentenceIndex]; 301 | // } 302 | // 303 | // //Verse number links 304 | // function verseLinkHover(verse) { 305 | // //alert(verse); 306 | // } 307 | // 308 | // function verseLinkClick(verse) { 309 | // ajaxGet("https://sefaria.org/api/name/" + verse).then((data) => 310 | // window.open("https://sefaria.org/" + data.url)) 311 | // } 312 | // 313 | // function imageButtonEnter(imageNumber) { 314 | // document.querySelector('.image-' + imageNumber).style.display = 'block'; 315 | // } 316 | // function imageButtonLeave(imageNumber) { 317 | // document.querySelector('.image-' + imageNumber).style.display = 'none'; 318 | // } 319 | 320 | 321 | function checkRashbam (tractate, daf, amud) { 322 | if(tractate === "Bava Batra" && daf >= 30){ 323 | return ("Rashbam"); 324 | } 325 | else{ 326 | return("Rashi"); 327 | } 328 | }; 329 | 330 | 331 | //This is our function to fetch data with Sefaria's API 332 | function jsonp(uri) { 333 | return new Promise(function (resolve, reject) { 334 | var id = '_' + Math.round(10000 * Math.random()); 335 | var callbackName = 'jsonp_callback_' + id; 336 | window[callbackName] = function (data) { 337 | delete window[callbackName]; 338 | var ele = document.getElementById(id); 339 | ele.parentNode.removeChild(ele); 340 | resolve(data); 341 | } 342 | 343 | var src = uri + '&callback=' + callbackName; 344 | var script = document.createElement('script'); 345 | script.src = src; 346 | script.id = id; 347 | script.addEventListener('error', reject); 348 | (document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script) 349 | }); 350 | } 351 | 352 | function ajaxGet(uri, log) { 353 | return new Promise(function(resolve, reject) { 354 | const request = new XMLHttpRequest(); 355 | request.open("GET", uri, true); 356 | request.send(); 357 | request.onreadystatechange = (e) => { 358 | if (request.readyState === XMLHttpRequest.DONE) { 359 | if (request.status === 200) { 360 | try { 361 | resolve(JSON.parse(request.response)); 362 | } catch (e) { 363 | resolve(request.response); 364 | } 365 | } else { 366 | reject(request.status); 367 | } 368 | } else { 369 | if (log) { 370 | console.log("Request readyState: " + request.readyState) 371 | } 372 | } 373 | } 374 | }); 375 | } 376 | 377 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daf-renderer", 3 | "version": "1.1.4", 4 | "description": "A DOM render library for creating Talmud pages on the web.", 5 | "main": "dist/daf-renderer.umd.js", 6 | "module": "dist/daf-renderer.esm.js", 7 | "browser": "dist/daf-renderer.umd.js", 8 | "scripts": { 9 | "build": "rollup -c -w" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Dan Jutan", 14 | "email": "danjutan@gmail.com" 15 | }, 16 | { 17 | "name": "Shaun Regenbaum", 18 | "email": "shaunregenbaum@gmail.com" 19 | } 20 | ], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "postcss": "^8.2.1", 24 | "rollup": "^2.36.1", 25 | "rollup-plugin-postcss": "^4.0.0", 26 | "rollup-plugin-terser": "^7.0.2" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/GT-Jewish-DH/daf-renderer.git" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | import postcss from 'rollup-plugin-postcss'; 3 | import {terser} from "rollup-plugin-terser"; 4 | export default [ 5 | // browser-friendly UMD build 6 | { 7 | input: 'src/renderer.js', 8 | output: [ 9 | { 10 | name: 'dafRenderer', 11 | file: pkg.browser, 12 | format: 'umd' 13 | }, 14 | { 15 | file: pkg.module, 16 | format: 'es' 17 | }, 18 | { 19 | name: "dafRenderer", 20 | file: "dist/daf-renderer.min.js", 21 | format: "umd", 22 | plugins: [terser()] 23 | } 24 | ], 25 | plugins: [ 26 | postcss({ 27 | modules: true, 28 | plugins: [] 29 | }) 30 | ] 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/calculate-spacers-breaks.js: -------------------------------------------------------------------------------- 1 | function getLineInfo(text, font, fontSize, lineHeight, dummy) { 2 | dummy.innerHTML = ""; 3 | let testDiv = document.createElement("span"); 4 | testDiv.style.font = fontSize + " " + String(font); 5 | testDiv.style.lineHeight = String(lineHeight) + "px"; 6 | testDiv.innerHTML = text; 7 | testDiv.style.position = "absolute"; 8 | dummy.append(testDiv); 9 | const rect = testDiv.getBoundingClientRect(); 10 | const height = rect.height; 11 | const width = rect.width; 12 | const widthProportional = width / dummy.getBoundingClientRect().width; 13 | testDiv.remove(); 14 | return {height, width, widthProportional}; 15 | } 16 | 17 | function heightAccumulator(font, fontSize, lineHeight, dummy) { 18 | return (lines) => { 19 | return getLineInfo(lines.join("
"), font, fontSize, lineHeight, dummy).height; 20 | } 21 | } 22 | 23 | function getBreaks(sizeArray) { 24 | const widths = sizeArray.map(size => size.widthProportional); 25 | const diffs = widths.map((width, index, widths) => index == 0 ? 0 : Math.abs(width - widths[index - 1])); 26 | const threshold = 0.12; 27 | let criticalPoints = diffs.reduce((indices, curr, currIndex) => { 28 | //Breaks before line 4 are flukes 29 | if (currIndex < 4) return indices; 30 | if (curr > threshold) { 31 | //There should never be two breakpoints in a row 32 | const prevIndex = indices[indices.length - 1]; 33 | if (prevIndex && (currIndex - prevIndex) == 1) { 34 | return indices; 35 | } 36 | indices.push(currIndex); 37 | } 38 | return indices; 39 | }, []); 40 | const averageAround = points => points.map((point, i) => { 41 | let nextPoint; 42 | if (!nextPoint) { 43 | nextPoint = Math.min(point + 3, widths.length - 1); 44 | } 45 | let prevPoint; 46 | if (!prevPoint) { 47 | prevPoint = Math.max(point - 3, 0); 48 | } 49 | /* 50 | Note that these are divided by the width of the critical point line such that 51 | we get the average width of the preceeding and proceeding chunks *relative* 52 | to the critical line. 53 | */ 54 | const before = (widths.slice(prevPoint, point).reduce((acc, curr) => acc + curr) / 55 | (point - prevPoint)) / widths[point]; 56 | let after; 57 | if ( point + 1 >= nextPoint) { 58 | after = widths[nextPoint] / widths[point]; 59 | } else { 60 | after =(widths.slice(point + 1, nextPoint).reduce((acc, curr) => acc + curr) / 61 | (nextPoint - point - 1)) / widths[point]; 62 | } 63 | return { 64 | point, 65 | before, 66 | after, 67 | diff: Math.abs(after - before) 68 | } 69 | }) 70 | const aroundDiffs = averageAround(criticalPoints) 71 | .sort( (a,b) => b.diff - a.diff); 72 | criticalPoints = aroundDiffs 73 | .filter( ({diff}) => diff > 0.22) 74 | .map( ({point}) => point) 75 | return criticalPoints.sort( (a, b) => a - b); 76 | } 77 | 78 | export function onlyOneCommentary(lines, options, dummy) { 79 | const fontFamily = options.fontFamily.inner; 80 | const fontSize = options.fontSize.side; 81 | const lineHeight = parseFloat(options.lineHeight.side); 82 | const sizes = lines.map(text => getLineInfo(text, fontFamily, fontSize, lineHeight, dummy)); 83 | const breaks = getBreaks(sizes); 84 | if (breaks.length == 3) { 85 | const first = lines.slice(0, breaks[1]); 86 | const second = lines.slice(breaks[1]); 87 | return [first, second]; 88 | } 89 | } 90 | 91 | export function calculateSpacersBreaks(mainArray, rashiArray, tosafotArray, options, dummy) { 92 | const lines = { 93 | main: mainArray, 94 | rashi: rashiArray, 95 | tosafot: tosafotArray 96 | } 97 | 98 | const parsedOptions = { 99 | padding: { 100 | vertical: parseFloat(options.padding.vertical), 101 | horizontal: parseFloat(options.padding.horizontal) 102 | }, 103 | halfway: 0.01 * parseFloat(options.halfway), 104 | fontFamily: options.fontFamily, // Object of strings 105 | fontSize: { 106 | main: options.fontSize.main, 107 | side: options.fontSize.side, 108 | }, 109 | lineHeight: { 110 | main: parseFloat(options.lineHeight.main), 111 | side: parseFloat(options.lineHeight.side), 112 | }, 113 | } 114 | 115 | 116 | const mainOptions = [parsedOptions.fontFamily.main, parsedOptions.fontSize.main, parsedOptions.lineHeight.main]; 117 | const commentaryOptions = [parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, parsedOptions.lineHeight.side]; 118 | 119 | const sizes = {}; 120 | sizes.main = lines.main.map(text => getLineInfo(text, ...mainOptions, dummy)); 121 | ["rashi", "tosafot"].forEach(text => { 122 | sizes[text] = lines[text].map(line => getLineInfo(line, ...commentaryOptions, dummy)) 123 | }) 124 | 125 | const accumulateMain = heightAccumulator(...mainOptions, dummy); 126 | const accumulateCommentary = heightAccumulator(...commentaryOptions, dummy); 127 | 128 | const breaks = {}; 129 | 130 | ["rashi", "tosafot", "main"].forEach(text => { 131 | breaks[text] = getBreaks(sizes[text]) 132 | /* 133 | Hadran lines aren't real candidates for line breaks. 134 | TODO: Extract this behavior , give it an option/parameter 135 | */ 136 | .filter(lineNum => !(lines[text][lineNum].includes("hadran")) 137 | ) 138 | }) 139 | 140 | 141 | const spacerHeights = { 142 | start: 4.4 * parsedOptions.lineHeight.side, 143 | inner: null, 144 | outer: null, 145 | end: 0, 146 | exception: 0 147 | }; 148 | 149 | const mainHeight = accumulateMain(lines.main); 150 | const mainHeightOld = (sizes.main.length) * parsedOptions.lineHeight.main; 151 | let afterBreak = { 152 | inner: accumulateCommentary(lines.rashi.slice(4)), 153 | outer: accumulateCommentary(lines.tosafot.slice(4)) 154 | } 155 | 156 | let afterBreakOld = { 157 | inner: parsedOptions.lineHeight.side * (sizes.rashi.length - 4), 158 | outer: parsedOptions.lineHeight.side * (sizes.tosafot.length - 4) 159 | } 160 | 161 | if (breaks.rashi.length < 1 || breaks.tosafot.length < 1) { 162 | console.log("Dealing with Exceptions") 163 | if (breaks.rashi.length < 1) { 164 | afterBreak.inner = parsedOptions.lineHeight.side * (sizes.rashi.length + 1) 165 | spacerHeights.exception = 2 166 | } 167 | if (breaks.tosafot.length < 1) { 168 | afterBreak.outer = parsedOptions.lineHeight.side * (sizes.tosafot.length + 1) 169 | spacerHeights.exception = 2 170 | } 171 | } 172 | switch (breaks.main.length) { 173 | case 0: 174 | spacerHeights.inner = mainHeight; 175 | spacerHeights.outer = mainHeight; 176 | if (breaks.rashi.length == 2) { 177 | spacerHeights.end = accumulateCommentary(lines.rashi.slice(breaks.rashi[1])) 178 | } else { 179 | spacerHeights.end = accumulateCommentary(lines.tosafot.slice(breaks.tosafot[1])) 180 | } 181 | console.log("Double wrap") 182 | break; 183 | case 1: 184 | if (breaks.rashi.length != breaks.tosafot.length) { 185 | if (breaks.tosafot.length == 0) { 186 | spacerHeights.outer = 0; 187 | spacerHeights.inner = afterBreak.inner; 188 | break; 189 | } 190 | if (breaks.rashi.length == 0) { 191 | spacerHeights.inner = 0; 192 | spacerHeights.outer = afterBreak.outer; 193 | break; 194 | } 195 | let stair; 196 | let nonstair; 197 | if (breaks.rashi.length == 1) { 198 | stair = "outer"; 199 | nonstair = "inner"; 200 | } else { 201 | stair = "inner"; 202 | nonstair = "outer"; 203 | } 204 | spacerHeights[nonstair] = afterBreak[nonstair]; 205 | spacerHeights[stair] = mainHeight; 206 | console.log("Stairs") 207 | break; 208 | } 209 | case 2: 210 | spacerHeights.inner = afterBreak.inner; 211 | spacerHeights.outer = afterBreak.outer; 212 | console.log("Double Extend") 213 | break; 214 | default: 215 | spacerHeights.inner = afterBreak.inner; 216 | spacerHeights.outer = afterBreak.outer; 217 | console.log("No Case Exception") 218 | break; 219 | } 220 | return spacerHeights; 221 | } 222 | -------------------------------------------------------------------------------- /src/calculate-spacers.js: -------------------------------------------------------------------------------- 1 | function getAreaOfText(text, font, fs, width, lh, dummy) { 2 | let testDiv = document.createElement("div"); 3 | testDiv.style.font = String(fs) + "px " + String(font); 4 | testDiv.style.width = String(width) + "px"; //You can remove this, but it may introduce unforseen problems 5 | testDiv.style.lineHeight = String(lh) + "px"; 6 | testDiv.innerHTML = text; 7 | dummy.append(testDiv); 8 | let test_area = Number(testDiv.clientHeight * testDiv.clientWidth); 9 | testDiv.remove(); 10 | return test_area; 11 | } 12 | 13 | function calculateSpacers(mainText, innerText, outerText, options, dummy) { 14 | 15 | const parsedOptions = { 16 | width: parseFloat(options.contentWidth), 17 | padding: { 18 | vertical: parseFloat(options.padding.vertical), 19 | horizontal: parseFloat(options.padding.horizontal) 20 | }, 21 | halfway: 0.01 * parseFloat(options.halfway), 22 | fontFamily: options.fontFamily, 23 | fontSize: { 24 | main: parseFloat(options.fontSize.main), 25 | side: parseFloat(options.fontSize.side), 26 | }, 27 | lineHeight: { 28 | main: parseFloat(options.lineHeight.main), 29 | side: parseFloat(options.lineHeight.side), 30 | }, 31 | mainWidth: 0.01 * parseFloat(options.mainWidth) 32 | } 33 | 34 | const midWidth = Number(parsedOptions.width * parsedOptions.mainWidth) - 2*parsedOptions.padding.horizontal; //main middle strip 35 | const topWidth = Number(parsedOptions.width * parsedOptions.halfway) - parsedOptions.padding.horizontal; //each commentary top 36 | const sideWidth = Number(parsedOptions.width * (1 - parsedOptions.mainWidth)/2) //each commentary widths, dont include padding, sokeep it constant 37 | 38 | const spacerHeights = { 39 | start: 4.3 * parsedOptions.lineHeight.side, 40 | inner: null, 41 | outer: null, 42 | end: 0, 43 | exception: 0 44 | }; 45 | 46 | // We are accounting for the special case, where you have line breaks: 47 | // if (options.lineBreaks) { 48 | // console.log("Special Case for Line Breaks") 49 | // const main = { 50 | // name: "main", 51 | // text: mainText, 52 | // lineHeight: parsedOptions.lineHeight.main, 53 | // top: 0, 54 | // } 55 | // const outer = { 56 | // name: "outer", 57 | // text: outerText, 58 | // lineHeight: parsedOptions.lineHeight.side, 59 | // top: 4, 60 | // } 61 | // const inner = { 62 | // name: "inner", 63 | // text: innerText, 64 | // lineHeight: parsedOptions.lineHeight.side, 65 | // top: 4, 66 | // } 67 | // 68 | // const texts = [main, outer, inner]; 69 | // texts.forEach(body => body.brCount = (body.text.match(/
/g) || []).length - body.top); 70 | // texts.forEach(body => body.height = (body.brCount * body.lineHeight)); 71 | // texts.forEach(body => body.unadjustedHeight = ((body.brCount + body.top + 1) * body.lineHeight)); 72 | // texts.forEach(body => body.unadjustedHeightAlt = ((body.brCount + body.top) * body.lineHeight)*sideWidth/topWidth); 73 | // const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 74 | // 75 | // const exConst = 2.2 76 | // 77 | // //Checking Exceptions: 78 | // if (inner.unadjustedHeight <= 0 && outer.unadjustedHeight <= 0){ 79 | // console.error("No Commentary"); 80 | // return Error("No Commentary"); 81 | // }; 82 | // if (inner.unadjustedHeightAlt/exConst < spacerHeights.start || outer.unadjustedHeightAlt/exConst < spacerHeights.start) { 83 | // console.log("Exceptions") 84 | // if (inner.unadjustedHeightAlt/exConst <= spacerHeights.start) { 85 | // spacerHeights.inner = inner.unadjustedHeight; 86 | // spacerHeights.outer = outer.height 87 | // return spacerHeights; 88 | // } 89 | // if (outer.unadjustedHeightAlt/exConst <= spacerHeights.start) { 90 | // spacerHeights.outer = outer.unadjustedHeight; 91 | // spacerHeights.inner = inner.height; 92 | // return spacerHeights; 93 | // } 94 | // else { 95 | // return Error("Inner Spacer Error"); 96 | // } 97 | // }; 98 | // //If Double=Wrap 99 | // if (perHeight[0].name === "main"){ 100 | // console.log("Double-Wrap"); 101 | // spacerHeights.inner = main.height; 102 | // spacerHeights.outer = main.height; 103 | // 104 | // const brDifference = perHeight[1].brCount - perHeight[0].brCount; 105 | // spacerHeights.end = brDifference*perHeight[1].lineHeight; 106 | // return spacerHeights; 107 | // } 108 | // 109 | // //If Stairs 110 | // if (perHeight[1].name === "main") { 111 | // console.log("Stairs"); 112 | // spacerHeights[perHeight[0].name] = perHeight[0].height; 113 | // spacerHeights[perHeight[2].name] = main.height; 114 | // return spacerHeights; 115 | // } 116 | // 117 | // //If Double Extend 118 | // console.log("Double-Extend") 119 | // spacerHeights.inner = inner.height + (inner.height/inner.height**2)*inner.lineHeight; 120 | // spacerHeights.outer = outer.height + (inner.height/inner.height**2)*outer.lineHeight; 121 | // return spacerHeights 122 | // } 123 | 124 | 125 | // We could probably put this somewhere else, it was meant to be a place for all the padding corrections, 126 | // but there turned out to only be one 127 | const paddingAreas = { 128 | name: "paddingAreas", 129 | horizontalSide: sideWidth * parsedOptions.padding.vertical, 130 | } 131 | 132 | 133 | const topArea = (lineHeight) => ((4 * lineHeight * topWidth)); //remove area of the top 4 lines 134 | 135 | 136 | const main = { 137 | name: "main", 138 | width: midWidth, 139 | text: mainText, 140 | lineHeight: parsedOptions.lineHeight.main, 141 | area: getAreaOfText(mainText, parsedOptions.fontFamily.main, parsedOptions.fontSize.main, midWidth, parsedOptions.lineHeight.main, dummy), 142 | length: null, 143 | height: null, 144 | } 145 | const outer = { 146 | name: "outer", 147 | width: sideWidth, 148 | text: outerText, 149 | lineHeight: parsedOptions.lineHeight.side, 150 | area: getAreaOfText(outerText, parsedOptions.fontFamily.outer, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 151 | - topArea(parsedOptions.lineHeight.side), 152 | length: null, 153 | height: null, 154 | } 155 | const inner = { 156 | name: "inner", 157 | width: sideWidth, 158 | text: innerText, 159 | lineHeight: parsedOptions.lineHeight.side, 160 | area: 161 | getAreaOfText(innerText, parsedOptions.fontFamily.inner, parsedOptions.fontSize.side, sideWidth, parsedOptions.lineHeight.side, dummy) 162 | - topArea(parsedOptions.lineHeight.side), 163 | length: null, 164 | height: null, 165 | } 166 | 167 | const texts = [main, outer, inner]; 168 | texts.forEach(text => text.height = text.area / text.width); 169 | texts.forEach(text => text.unadjustedArea = text.area + topArea(parsedOptions.lineHeight.side)); 170 | texts.forEach(text => text.unadjustedHeight = text.unadjustedArea / text.width); 171 | 172 | const perHeight = Array.from(texts).sort((a, b) => a.height - b.height); 173 | 174 | //There are Three Main Types of Case: 175 | //Double-Wrap: The main text being the smallest and commentaries wrapping around it 176 | //Stairs: The main text wrapping around one, but the other wrapping around it 177 | //Double-Extend: The main text wrapping around both commentaries 178 | 179 | //Main Text is Smallest: Double-Wrap 180 | //Main Text being Middle: Stairs 181 | //Main Text Being Largest: Double-Extend 182 | 183 | //First we need to check we have enough commentary to fill the first four lines 184 | if (inner.height <= 0 && outer.height <= 0){ 185 | console.error("No Commentary"); 186 | return Error("No Commentary"); 187 | }; 188 | 189 | 190 | // This is a case that we have to decice what to do with, when there is not enough commentary on both sides to fill the lines. 191 | if (inner.height <= spacerHeights.start && outer.height <= spacerHeights.start) { 192 | console.error("Not Enough Commentary to Fill Four Lines"); 193 | return Error("Not Enough Commentary"); 194 | }; 195 | 196 | // We are going to deal with our first edge case when there is either only one commentary 197 | // Or where there is enough of one commentary, but not four lines of the other. 198 | if (inner.unadjustedHeight <= spacerHeights.start || outer.unadjustedHeight <= spacerHeights.start) { 199 | if (inner.unadjustedHeight <= spacerHeights.start) { 200 | spacerHeights.inner = inner.unadjustedHeight; 201 | spacerHeights.outer = (outer.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 202 | spacerHeights.exception = 1; 203 | return spacerHeights; 204 | } 205 | if (outer.unadjustedHeight <= spacerHeights.start) { 206 | spacerHeights.outer = outer.unadjustedHeight; 207 | 208 | spacerHeights.inner = (inner.unadjustedArea - parsedOptions.width * 4 * parsedOptions.lineHeight.side) / sideWidth; 209 | spacerHeights.exception = 2; 210 | return spacerHeights; 211 | } 212 | else { 213 | return Error("Inner Spacer Error"); 214 | } 215 | }; 216 | 217 | //If Double=Wrap 218 | if (perHeight[0].name === "main"){ 219 | console.log("Double-Wrap"); 220 | spacerHeights.inner = main.area/midWidth; 221 | spacerHeights.outer = spacerHeights.inner; 222 | 223 | const sideArea = spacerHeights.inner * sideWidth + paddingAreas.horizontalSide; 224 | const bottomChunk = perHeight[1].area - sideArea; 225 | const bottomHeight = bottomChunk / topWidth; 226 | spacerHeights.end = bottomHeight; 227 | return spacerHeights; 228 | } 229 | // If Stairs, there's one text at the bottom. We will call it THE stair. 230 | // The remaining two texts form a "block" that we must compare with that bottom text. 231 | const blockArea = (main.area + perHeight[0].area); 232 | const blockWidth = midWidth + sideWidth; 233 | const blockHeight = blockArea / blockWidth; 234 | 235 | const stair = (perHeight[1].name == "main") ? perHeight[2] : perHeight[1]; 236 | const stairHeight = stair.area / stair.width; 237 | 238 | if (blockHeight < stairHeight) { 239 | console.log(`Stairs, ${stair.name} is the stair`); 240 | // This function accounts for extra space that is introduced by padding 241 | const lilArea = (height1, height2, horizPadding) => (horizPadding) * (height1 - height2); 242 | const smallest = perHeight[0]; 243 | spacerHeights[smallest.name] = smallest.height; 244 | spacerHeights[stair.name] = (blockArea - lilArea(blockHeight, spacerHeights[smallest.name], parsedOptions.padding.horizontal)) / blockWidth; 245 | return spacerHeights 246 | } 247 | //If Double Extend 248 | console.log("Double-Extend") 249 | spacerHeights.inner = inner.height; 250 | spacerHeights.outer = outer.height; 251 | 252 | return spacerHeights 253 | } 254 | 255 | 256 | export default (calculateSpacers); -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | contentWidth: "600px", 3 | mainWidth: "50%", 4 | padding: { 5 | vertical: "10px", 6 | horizontal: "16px", 7 | }, 8 | innerPadding: "4px", 9 | outerPadding: "4px", 10 | halfway: "50%", 11 | fontFamily: { 12 | inner: "Rashi", 13 | outer: "Rashi", 14 | main: "Vilna" 15 | }, 16 | direction: "rtl", 17 | fontSize: { 18 | main: "15px", 19 | side: "10.5px" 20 | }, 21 | lineHeight: { 22 | main: "17px", 23 | side: "14px", 24 | } 25 | } 26 | 27 | function mergeAndClone (modified, definitional = defaultOptions) { 28 | const newOptions = {}; 29 | for (const key in definitional) { 30 | if (key in modified) { 31 | const defType = typeof definitional[key]; 32 | if (typeof modified[key] !== defType) { 33 | console.error(`Option ${key} must be of type ${defType}; ${typeof modified[key]} was passed.`); 34 | } 35 | if (defType == "object") { 36 | newOptions[key] = mergeAndClone(modified[key], definitional[key]) 37 | } else { 38 | newOptions[key] = modified[key]; 39 | } 40 | } else { 41 | newOptions[key] = definitional[key]; 42 | } 43 | } 44 | return newOptions; 45 | } 46 | 47 | export {defaultOptions, mergeAndClone} 48 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import {defaultOptions, mergeAndClone} from "./options"; 2 | import calculateSpacers from "./calculate-spacers"; 3 | import styleManager from "./style-manager"; 4 | import { 5 | calculateSpacersBreaks, 6 | onlyOneCommentary 7 | } from "./calculate-spacers-breaks"; 8 | import classes from './styles.css'; 9 | 10 | function el(tag, parent) { 11 | const newEl = document.createElement(tag); 12 | if (parent) parent.append(newEl); 13 | return newEl; 14 | } 15 | 16 | function div(parent) { 17 | return el("div", parent); 18 | } 19 | 20 | function span(parent) { 21 | return el("span", parent); 22 | } 23 | 24 | 25 | export default function (el, options = defaultOptions) { 26 | const root = (typeof el === "string") ? document.querySelector(el) : el; 27 | if (!(root && root instanceof Element && root.tagName.toUpperCase() === "DIV")) { 28 | throw "Argument must be a div element or its selector" 29 | } 30 | const outerContainer = div(root); 31 | const innerContainer = div(root); 32 | const mainContainer = div(root); 33 | const dummy = div(root); 34 | dummy.id = "dummy"; 35 | const containers = { 36 | el: root, 37 | dummy: dummy, 38 | outer: { 39 | el: outerContainer, 40 | spacers: { 41 | start: div(outerContainer), 42 | mid: div(outerContainer), 43 | end: div(outerContainer) 44 | }, 45 | text: div(outerContainer) 46 | }, 47 | inner: { 48 | el: innerContainer, 49 | spacers: { 50 | start: div(innerContainer), 51 | mid: div(innerContainer), 52 | end: div(innerContainer) 53 | }, 54 | text: div(innerContainer) 55 | }, 56 | main: { 57 | el: mainContainer, 58 | spacers: { 59 | start: div(mainContainer), 60 | inner: div(mainContainer), 61 | outer: div(mainContainer), 62 | }, 63 | text: div(mainContainer) 64 | } 65 | } 66 | 67 | const textSpans = { 68 | main: span(containers.main.text), 69 | inner: span(containers.inner.text), 70 | outer: span(containers.outer.text) 71 | } 72 | 73 | const clonedOptions = mergeAndClone(options, defaultOptions); 74 | 75 | styleManager.applyClasses(containers); 76 | styleManager.updateOptionsVars(clonedOptions); 77 | 78 | let resizeEvent; 79 | return { 80 | classes, 81 | containers, 82 | spacerHeights: { 83 | start: 0, 84 | inner: 0, 85 | outer: 0, 86 | end: 0 87 | }, 88 | amud: "a", 89 | render(main, inner, outer, amud = "a", linebreak, renderCallback, resizeCallback) { 90 | if (resizeEvent) { 91 | window.removeEventListener("resize", resizeEvent); 92 | } 93 | if (this.amud != amud) { 94 | this.amud = amud; 95 | styleManager.updateIsAmudB(amud == "b"); 96 | } 97 | if (!linebreak) { 98 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 99 | resizeEvent = () => { 100 | this.spacerHeights = calculateSpacers(main, inner, outer, clonedOptions, containers.dummy); 101 | styleManager.updateSpacersVars(this.spacerHeights); 102 | console.log("resizing"); 103 | if (resizeCallback) 104 | resizeCallback(); 105 | } 106 | window.addEventListener("resize", resizeEvent); 107 | } 108 | else { 109 | let [mainSplit, innerSplit, outerSplit] = [main, inner, outer].map( text => { 110 | containers.dummy.innerHTML = text; 111 | const divRanges = Array.from(containers.dummy.querySelectorAll("div")).map(div => { 112 | const range = document.createRange(); 113 | range.selectNode(div); 114 | return range; 115 | }) 116 | 117 | const brs = containers.dummy.querySelectorAll(linebreak); 118 | const splitFragments = [] 119 | brs.forEach((node, index) => { 120 | const range = document.createRange(); 121 | range.setEndBefore(node); 122 | if (index == 0) { 123 | range.setStart(containers.dummy, 0); 124 | } else { 125 | const prev = brs[index - 1]; 126 | range.setStartAfter(prev); 127 | } 128 | divRanges.forEach( (divRange, i) => { 129 | const inBetween = range.compareBoundaryPoints(Range.START_TO_START, divRange) < 0 && range.compareBoundaryPoints(Range.END_TO_END, divRange) > 0; 130 | if (inBetween) { 131 | splitFragments.push(divRange.extractContents()); 132 | divRanges.splice(i, 1); 133 | } 134 | }); 135 | 136 | splitFragments.push(range.extractContents()); 137 | }) 138 | 139 | return splitFragments.map(fragment => { 140 | const el = document.createElement("div"); 141 | el.append(fragment); 142 | return el.innerHTML; 143 | }) 144 | }); 145 | 146 | containers.dummy.innerHTML = ""; 147 | 148 | const hasInner = innerSplit.length != 0; 149 | const hasOuter = outerSplit.length != 0; 150 | 151 | if (hasInner != hasOuter) { 152 | const withText = hasInner ? innerSplit : outerSplit; 153 | const fixed = onlyOneCommentary(withText, clonedOptions, dummy); 154 | if (fixed) { 155 | if (amud == "a") { 156 | innerSplit = fixed[0]; 157 | outerSplit = fixed[1]; 158 | } else { 159 | innerSplit = fixed[1]; 160 | outerSplit = fixed[0]; 161 | } 162 | inner = innerSplit.join('
'); 163 | outer = outerSplit.join('
'); 164 | } 165 | } 166 | 167 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 168 | resizeEvent = () => { 169 | this.spacerHeights = calculateSpacersBreaks(mainSplit, innerSplit, outerSplit, clonedOptions, containers.dummy); 170 | styleManager.updateSpacersVars(this.spacerHeights); 171 | if (resizeCallback) 172 | resizeCallback(); 173 | console.log("resizing") 174 | } 175 | window.addEventListener('resize', resizeEvent) 176 | } 177 | styleManager.updateSpacersVars(this.spacerHeights); 178 | styleManager.manageExceptions(this.spacerHeights); 179 | textSpans.main.innerHTML = main; 180 | textSpans.inner.innerHTML = inner; 181 | textSpans.outer.innerHTML = outer; 182 | 183 | const containerHeight = Math.max(...["main", "inner", "outer"].map(t => containers[t].el.offsetHeight)); 184 | containers.el.style.height = `${containerHeight}px`; 185 | if (renderCallback) 186 | renderCallback(); 187 | }, 188 | } 189 | } 190 | 191 | -------------------------------------------------------------------------------- /src/style-manager.js: -------------------------------------------------------------------------------- 1 | import classes from './styles.css'; 2 | 3 | const sideSpacersClasses = { 4 | start: [classes.spacer, classes.start], 5 | mid: [classes.spacer, classes.mid], 6 | end: [classes.spacer, classes.end] 7 | } 8 | 9 | const containerClasses = { 10 | el: classes.dafRoot, 11 | outer: { 12 | el: classes.outer, 13 | spacers: sideSpacersClasses, 14 | text: classes.text, 15 | }, 16 | inner: { 17 | el: classes.inner, 18 | spacers: sideSpacersClasses, 19 | text: classes.text, 20 | }, 21 | main: { 22 | el: classes.main, 23 | spacers: { 24 | start: sideSpacersClasses.start, 25 | inner: [classes.spacer, classes.innerMid], 26 | outer: [classes.spacer, classes.outerMid] 27 | }, 28 | text: classes.text 29 | } 30 | } 31 | 32 | function addClasses(element, classNames) { 33 | if (Array.isArray(classNames)) 34 | element.classList.add(...classNames) 35 | else 36 | element.classList.add(classNames) 37 | } 38 | 39 | function setVars(object, prefix = "") { 40 | const varsRule = Array.prototype.find.call 41 | (document.styleSheets, sheet => sheet.rules[0].selectorText == `.${classes.dafRoot}`) 42 | .rules[0]; 43 | 44 | Object.entries(object).forEach(([key, value]) => { 45 | if (typeof value == "string") { 46 | varsRule.style.setProperty(`--${prefix}${key}`, value); 47 | } else if (typeof value == "object") { 48 | setVars(value, `${key}-`); 49 | } 50 | }) 51 | } 52 | 53 | 54 | let appliedOptions; 55 | export default { 56 | applyClasses(containers, classesMap = containerClasses) { 57 | for (const key in containers) { 58 | if (key in classesMap) { 59 | const value = classesMap[key]; 60 | if (typeof value === "object" && !Array.isArray(value)) { 61 | this.applyClasses(containers[key], value); 62 | } else { 63 | addClasses(containers[key], value); 64 | } 65 | } 66 | } 67 | }, 68 | updateOptionsVars(options) { 69 | appliedOptions = options; 70 | setVars(options) 71 | }, 72 | updateSpacersVars(spacerHeights) { 73 | setVars( 74 | Object.fromEntries( 75 | Object.entries(spacerHeights).map( 76 | ([key, value]) => ([key, String(value) + 'px'])) 77 | ), 78 | "spacerHeights-" 79 | ); 80 | }, 81 | updateIsAmudB(amudB) { 82 | setVars({ 83 | innerFloat: amudB ? "right" : "left", 84 | outerFloat: amudB ? "left" : "right" 85 | }) 86 | }, 87 | manageExceptions(spacerHeights) { 88 | if (!spacerHeights.exception) { 89 | setVars({ 90 | hasOuterStartGap: "0", 91 | hasInnerStartGap: "0", 92 | outerStartWidth: "50%", 93 | innerStartWidth: "50%", 94 | innerPadding: appliedOptions.innerPadding, 95 | outerPadding: appliedOptions.outerPadding, 96 | }); 97 | return; 98 | } 99 | if (spacerHeights.exception === 1) { 100 | console.log("In Style Exception, Case 1") 101 | setVars({ 102 | hasInnerStartGap: "1", 103 | innerStartWidth: "100%", 104 | outerStartWidth: "0%", 105 | innerPadding: "0px", 106 | outerPadding: "0px", 107 | }) 108 | } else if (spacerHeights.exception === 2) { 109 | console.log("In Style Exception, Case 2") 110 | setVars({ 111 | hasOuterStartGap: "1", 112 | outerStartWidth: "100%", 113 | innerStartWidth: "0%", 114 | innerPadding: "0px", 115 | outerPadding: "0px" 116 | }) 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /*Keep this as the first rule in the file*/ 2 | .dafRoot { 3 | --contentWidth: 0px; 4 | --padding-horizontal: 0px; 5 | --padding-vertical: 0px; 6 | --halfway: 50%; 7 | 8 | --fontFamily-inner: "Rashi"; 9 | --fontFamily-outer: "Tosafot"; 10 | --fontFamily-main: "Vilna"; 11 | --direction: "rtl"; 12 | 13 | --fontSize-main: 0px; 14 | --fontSize-side: 0px; 15 | 16 | --lineHeight-main: 0px; 17 | --lineHeight-side: 0px; 18 | 19 | --mainWidth: 0%; 20 | --mainMargin-start: var(--mainWidth); 21 | --sidePercent: calc(calc(100% - var(--mainMargin-start)) / 2); 22 | --remainderPercent: calc(100% - var(--sidePercent)); 23 | 24 | --innerFloat: left; 25 | --outerFloat: right; 26 | 27 | --spacerHeights-start: 0px; 28 | --spacerHeights-outer: 0px; 29 | --spacerHeights-inner: 0px; 30 | --spacerHeights-end: 0px; 31 | 32 | /*Edge Cases*/ 33 | --hasInnerStartGap: 0; 34 | --hasOuterStartGap: 0; 35 | --innerStartWidth: 50%; 36 | --innerPadding: 0px; 37 | --outerStartWidth: 50%; 38 | --outerPadding: 0px; 39 | } 40 | 41 | /*Containers*/ 42 | .dafRoot, 43 | .outer, 44 | .inner, 45 | .main { 46 | width: var(--contentWidth); 47 | pointer-events: none; 48 | box-sizing: content-box; 49 | } 50 | 51 | .outer, .inner, .main { 52 | position: absolute; 53 | } 54 | 55 | /*Float changes with amud*/ 56 | .inner .spacer, 57 | .main .spacer.outerMid { 58 | float: var(--innerFloat); 59 | } 60 | 61 | .outer .spacer, 62 | .main .spacer.innerMid { 63 | float: var(--outerFloat); 64 | } 65 | 66 | /*Spacer widths determined by options*/ 67 | .inner .spacer, 68 | .outer .spacer { 69 | width: var(--halfway); 70 | } 71 | .spacer.mid { 72 | width: var(--remainderPercent); 73 | } 74 | 75 | .main .spacer.start { 76 | width: var(--contentWidth); 77 | } 78 | .main .spacer.innerMid, 79 | .main .spacer.outerMid { 80 | width: var(--sidePercent); 81 | } 82 | 83 | /*Spacer heights determined by algorithm*/ 84 | .spacer.start { 85 | height: var(--spacerHeights-start); 86 | } 87 | 88 | .spacer.end { 89 | height: var(--spacerHeights-end); 90 | } 91 | 92 | .inner .spacer.mid, 93 | .main .spacer.innerMid { 94 | height: var(--spacerHeights-inner); 95 | } 96 | .outer .spacer.mid, 97 | .main .spacer.outerMid { 98 | height: var(--spacerHeights-outer); 99 | } 100 | 101 | /*Settings to handle edge Cases*/ 102 | 103 | .inner .spacer.start { 104 | width: var(--innerStartWidth); 105 | margin-left: var(--innerPadding); 106 | margin-right: var(--innerPadding); 107 | } 108 | 109 | .outer .spacer.start { 110 | width: var(--outerStartWidth); 111 | margin-left: var(--outerPadding); 112 | margin-right: var(--outerPadding); 113 | } 114 | 115 | .inner .spacer.start{ 116 | margin-bottom: calc(var(--padding-vertical) * var(--hasInnerStartGap)); 117 | } 118 | .outer .spacer.start { 119 | margin-bottom: calc(var(--padding-vertical) * var(--hasOuterStartGap)); 120 | } 121 | 122 | .spacer.mid { 123 | clear: both; 124 | } 125 | 126 | /*Margins!*/ 127 | .spacer.start, 128 | .spacer.end, 129 | .main .spacer.innerMid, 130 | .main .spacer.outerMid { 131 | margin-left: calc(0.5 * var(--padding-horizontal)); 132 | margin-right: calc(0.5 * var(--padding-horizontal)); 133 | } 134 | 135 | .spacer.mid, 136 | .main .text { 137 | margin-top: var(--padding-vertical); 138 | } 139 | 140 | .spacer.mid, 141 | .main .spacer.innerMid, 142 | .main .spacer.outerMid { 143 | margin-bottom: var(--padding-vertical); 144 | } 145 | 146 | /*Text*/ 147 | .text { 148 | direction: var(--direction); 149 | text-align: justify; 150 | text-align-last: center; 151 | } 152 | 153 | .text span { 154 | pointer-events: auto; 155 | } 156 | 157 | .main .text { 158 | font-family: var(--fontFamily-main); 159 | font-size: var(--fontSize-main); 160 | line-height: var(--lineHeight-main); 161 | } 162 | 163 | .inner .text, 164 | .outer .text { 165 | font-size: var(--fontSize-side); 166 | line-height: var(--lineHeight-side); 167 | } 168 | 169 | .inner .text { 170 | font-family: var(--fontFamily-inner); 171 | } 172 | 173 | .outer .text { 174 | font-family: var(--fontFamily-outer); 175 | } 176 | --------------------------------------------------------------------------------