四零语境前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
13 KiB

  1. <html>
  2. <head>
  3. <meta
  4. name="viewport"
  5. content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
  6. />
  7. <title>计算文本</title>
  8. <style type="text/css">
  9. html,body {
  10. margin: 0;
  11. padding: 0;
  12. width: 100%;
  13. height: 100%;
  14. overflow: hidden;
  15. box-sizing: border-box;
  16. }
  17. .yingbing-computed-wrapper {
  18. width: 100%;
  19. height: 100%;
  20. box-sizing: border-box;
  21. }
  22. .yingbing-img {
  23. max-width: 100%!important;
  24. max-height: 100%!important;
  25. }
  26. </style>
  27. </head>
  28. <body>
  29. <div class="yingbing-computed-wrapper"></div>
  30. </body>
  31. <script type="text/javascript" src="./js/uni-webview-js@1.5.4.js"></script>
  32. <script type="text/javascript">
  33. var htmlChars = ['㊦', '㊟', '㊨', '㊧'],
  34. windowHeight = 0,//窗口高度
  35. pages = [],//渲染页面数组
  36. chapter = {},//章节内容临时存储
  37. contents = [],//内容转化数组
  38. startTags = [],//临时存储被切割的开始标签
  39. handleTimer = null//定时处理器
  40. fontSize = 0,
  41. fontFamily = '',
  42. lineHeight = 0,
  43. totalChapter = 0
  44. //外层包裹div,设置公共样式(为了兼容微信小程序)
  45. function getContent (content) {
  46. return `<div class="yingbing-computed-content-html" style="font-family:${fontFamily};font-size:${fontSize}px;line-height:${lineHeight}px;">` + content.replace('<whole-render', '<div').replace('<\/whole-render>', '<\/div>') + '</div>'
  47. }
  48. function start (c, s) {
  49. destroyHandleTimer()
  50. chapter = c//记录章节内容
  51. fontSize = s.fontSize
  52. fontFamily = s.fontFamily
  53. lineHeight = s.lineHeight
  54. totalChapter = s.totalChapter
  55. windowHeight = document.body.offsetHeight;
  56. if ( !chapter.content ) {
  57. handleSuccess()
  58. return
  59. }
  60. let content = chapter.content
  61. const fullStyle = 'box-sizing:border-box;overflow:hidden;height:' + windowHeight + 'px'//full-box的样式
  62. content = content.replace(/<full-box.*?>/g, function(item){
  63. if(item.includes('style=')){
  64. return item.replace(/style="(.*?)(?=\")/, 'style="' + fullStyle +';$1');
  65. }else{
  66. return item.replace(/\<full-box/, '<full-box style="'+fullStyle+'" $1');
  67. }
  68. })//给所有full-box添加全部高度
  69. content = content.replace(/<full-box/g, '<whole-render')//将full-box标签替换为whole-render
  70. content = content.replace(/<\/full-box>/g, '</whole-render>')//将full-box标签替换为whole-render
  71. const imgClassName = 'yingbing-img'//img类名
  72. content = content.replace(/<img.*?>/g, function(item){
  73. if(item.includes('class=')){
  74. return item.replace(/class="(.*?)(?=\")/, 'class="$1 '+imgClassName);
  75. }else{
  76. return item.replace(/\<img (.*?)(?=\")/, '<img class="'+imgClassName+'" $1');
  77. }
  78. })//给所有img添加class[yingbing-img] 设置img公共样式(为了兼容微信小程序)
  79. content = content.replace(/\r|\n/g, '<br/>')//将\n|\r替换为换行符
  80. content = content.replace(/\t/g, '')//将\t删除
  81. contents = contentToArr(content)//初始化内容,并将文本内容转为数组
  82. computedPage()
  83. }
  84. //将文本内容转为数组
  85. function contentToArr (content) {
  86. let temp = null
  87. let originContent = content
  88. const wholeRegTags= ['whole-render', 'full-box', 'button', 'video', 'audio', 'iframe']//需要整块渲染的标签
  89. const singleRegTags = ['img', 'hr', 'input', 'video', 'audio', 'iframe']//非闭合标签
  90. const wrapTags = [], wholeTags = [], endTags = [], startTags = []
  91. //将整块渲染的标签替换为㊟符号保存
  92. const wholeTagReg = new RegExp(`<(${wholeRegTags.join('|')})>*([\\s\\S]*?)<\/(${wholeRegTags.join('|')})>|<(${singleRegTags.join('|')})[^>]+>`)
  93. while ( (temp = originContent.match(wholeTagReg)) != null ) {
  94. wholeTags.push(temp[0])
  95. originContent = originContent.substring(0, temp.index) + htmlChars[1] + originContent.substring(temp.index + temp[0].length)
  96. }
  97. //将换行标签替换为㊦符号保存
  98. const wrapTagReg = new RegExp(`<br[^>]+>`)
  99. while ( (temp = originContent.match(wrapTagReg)) != null ) {
  100. wrapTags.push(temp[0])
  101. originContent = originContent.substring(0, temp.index) + htmlChars[0] + originContent.substring(temp.index + temp[0].length)
  102. }
  103. //将结束标签替换为㊨符号保存
  104. const endTagReg = new RegExp(`<\/[^>]+>`)//结束标签
  105. while ( (temp = originContent.match(endTagReg)) != null ) {
  106. endTags.push(temp[0])
  107. originContent = originContent.substring(0, temp.index) + htmlChars[2] + originContent.substring(temp.index + temp[0].length)
  108. }
  109. //将开始标签替换为㊧符号保存
  110. const startTagReg = new RegExp(`<[^>]+>`)//开始标签
  111. while ( (temp = originContent.match(startTagReg)) != null ) {
  112. startTags.push(temp[0])
  113. originContent = originContent.substring(0, temp.index) + htmlChars[3] + originContent.substring(temp.index + temp[0].length)
  114. }
  115. //将字符内容分割为数组
  116. const arr = originContent.split('')
  117. return arr.map((char, key) => {
  118. if ( htmlChars.indexOf(char) == -1 ) return char//非标签内容直接返回
  119. else {//标签内容转化为真实标签再返回
  120. let index = -1
  121. for ( let j = 0; j < key + 1; j++ ) if ( originContent.charAt(j) == char ) index++
  122. const tags = char == '㊦' ? wrapTags : char == '㊟' ? wholeTags : char == '㊨' ? endTags : startTags
  123. return index > -1 ? char + ':' + tags[index] : char
  124. }
  125. })
  126. }
  127. //计算页面
  128. function computedPage () {
  129. const start = pages.length > 0 ? pages[pages.length-1].end : 0//获取字符开始位置
  130. //新增页面
  131. pages.push( Object.assign({}, chapter, {
  132. content: '',
  133. contents: [],
  134. type: 'text',
  135. start: start,
  136. end: 0,
  137. }) )
  138. const div = document.createElement('DIV')
  139. div.setAttribute('class', 'yingbing-page-' + pages.length)
  140. document.querySelector('.yingbing-computed-wrapper').appendChild(div)
  141. //给页面添加内容
  142. addText(start)
  143. }
  144. //增加内容
  145. function addText (start) {
  146. let text = '',//渲染内容临时存储
  147. end = 0,//结束位置
  148. lt = startTags.length,//开始标签数量
  149. rt = 0//结束标签数量
  150. if ( startTags.length > 0 ) text += startTags.map(t => t.split('+')[1]).join('')//如果存在被切割的开始标签添加到内容最前面
  151. //截取字符 渲染
  152. for ( let i = start; i < contents.length; i++ ) {
  153. end = i//记录结束位置
  154. const char = contents[i]
  155. text += char//拼接文本
  156. if ( htmlChars.findIndex(c => char.indexOf(c)) > -1 ) {//如果是标签内容
  157. if ( char.indexOf(htmlChars[0] + ':') > -1 && lt == rt ) break;//如是换行标签并且开始标签和结束标签数量一致时直接开始渲染
  158. if ( char.indexOf(htmlChars[3] + ':') > -1 ) lt++//开始标签+1
  159. if ( char.indexOf(htmlChars[2] + ':') > -1 ) rt++//结束标签+1
  160. if ( lt == rt ) break;//如果开始标签数量等于结束标签则直接渲染
  161. }
  162. }
  163. startTags = []
  164. pages[pages.length-1].content += text.replace(new RegExp(`(${htmlChars.join('|')}):`, 'g'), '')// 开始渲染内容
  165. pages[pages.length-1].end = end//记录结束位置
  166. const handle = () => {
  167. destroyHandleTimer()
  168. if ( height <= windowHeight ) {//如果内容高度小于或等于窗口高度
  169. if ( end < contents.length - 1 ) {//如果内容还没有渲染完
  170. const subheight = windowHeight - height//页面剩余高度
  171. if ( subheight > lineHeight ) addText(end+1)//当前剩余高度大于行高,则继续增加内容
  172. else {
  173. pages[pages.length-1].end = end + 1//如果是增加内容时完成本页计算,结束位置要加1
  174. computedPage()//否则开始计算下一页
  175. }
  176. } else {
  177. pages[pages.length-1].end = end + 1//如果是增加内容时完成章节计算,结束位置要加1
  178. handleSuccess()//页面计算完毕开始返回
  179. }
  180. } else {
  181. //如果内容高度大于窗口高度开始减少内容
  182. subText()
  183. }
  184. }
  185. let height = 0
  186. const div = document.querySelector('.yingbing-page-' + pages.length)
  187. div.innerHTML = getContent(pages[pages.length-1].content)
  188. if ( /<(img|video|audio|iframe)/.test(text)) {
  189. handleTimer = window.setTimeout(() => {
  190. height = div.offsetHeight
  191. handle()
  192. }, 1000)
  193. } else {
  194. height = div.offsetHeight
  195. handle()
  196. }
  197. }
  198. //减少内容
  199. function subText () {
  200. const page = pages[pages.length - 1]//获取最后一页渲染数据
  201. let contents1 = contentToArr(page.content),//将当前页内容转化为数组
  202. end = page.end,//结束位置减1
  203. index = 0,//字符索引
  204. endTags = []//结束字符临时存储
  205. contents1.reverse()//反转文本内容集合
  206. const handle = () => {
  207. let lts = [],//存储开始标签
  208. rts = []//存储结束标签
  209. end--//结束位置减1
  210. const char = contents1[index]//获取文本内容最后一位
  211. if ( char.indexOf(htmlChars[2] + ':') > -1 ) {//如果最后一位是结束标签
  212. let startIndex = 0//存储结束标签对应开始标签的位置
  213. for ( let i = index; i < contents1.length; i++ ) {//循环字符集合找到结束标签对应的开始标签
  214. const char1 = contents1[i]
  215. if ( char1.indexOf(htmlChars[2] + ':') > -1 ) rts.unshift(char1)//如果标签是结束标签加入rts集合
  216. if ( char1.indexOf(htmlChars[3] + ':') > -1 ) lts.push(char1)//如果标签是开始标签加入lts集合
  217. if ( lts.length == rts.length ) break//如果开始标签数量等于结束标签时中段循环
  218. startIndex = i
  219. }
  220. if ( lts.length > 0 ) startTags.push(startIndex + '+' + lts[lts.length-1])//将结束最后一位结束标签对应的开始标签加入startTags
  221. endTags.unshift(startIndex + '+' + char)//存储结束标签,用于最后的拼接
  222. }
  223. if ( char.indexOf(htmlChars[3] + ':') > -1 ) {//如果最后一位是开始标签
  224. const startIndex = startTags.findIndex(t => t.indexOf(index + '+') > -1)//获取当前开始标签在startTags中的位置
  225. const endIndex = endTags.findIndex(t => t.indexOf(index + '+') > -1)//获取当前开始标签在endTags中对应的结束标签位置
  226. startTags.splice(startIndex, 1)//删除记录的开始标签
  227. endTags.splice(endIndex, 1)//删除记录的当前开始标签对应的结束标签
  228. }
  229. let contents2 = JSON.parse(JSON.stringify(contents1.slice(index)))//获取文本集合,并从索引位置开始截取
  230. contents2 = contents2.slice(1)//去掉最后一位
  231. contents2.reverse()//反转文本
  232. const text = contents2.join('') + endTags.map(t => t.split('+')[1]).join('')//拼接结束标签
  233. pages[pages.length - 1].content = text.replace(new RegExp(`(${htmlChars.join('|')}):`, 'g'), '')//渲染文本
  234. pages[pages.length - 1].end = end + 1//记录结束位置
  235. const div = document.querySelector('.yingbing-page-' + pages.length)
  236. div.innerHTML = getContent(pages[pages.length-1].content)
  237. const height = div.offsetHeight
  238. if ( height > windowHeight ) {//如果内容高度大于窗口高度
  239. index++//索引+1
  240. handle()//继续减少文本内容
  241. } else {
  242. if ( end < contents.length - 1 ) computedPage()//如果内容还没有渲染完毕继续计算下一页
  243. else handleSuccess()//页面计算完毕开始返回
  244. }
  245. }
  246. handle()
  247. }
  248. //成功回调
  249. function handleSuccess () {
  250. const slots1 = chapter.frontSlots || []//获取章节前置插槽
  251. const slots2 = chapter.backSlots || []//获取章节后置插槽
  252. slots1.reverse()//反转前置插槽
  253. //插入前置插槽
  254. slots1.forEach(name => {
  255. const start = pages.length > 0 ? pages[0].start : 2
  256. pages.unshift(Object.assign({}, chapter, {
  257. type: 'slot',
  258. content: name + ':' + chapter.index,
  259. start: start - 2,
  260. end: start - 1,
  261. }))
  262. })
  263. //插入后置插槽
  264. slots2.forEach(name => {
  265. const end = pages.length > 0 ? pages[pages.length - 1].end : -1
  266. pages.push(Object.assign({}, chapter, {
  267. type: 'slot',
  268. content: name + ':' + chapter.index,
  269. start: end + 1,
  270. end: end + 2,
  271. }))
  272. })
  273. pages = pages.map((p, key) => {
  274. const total = pages.length
  275. const current = key + 1
  276. const rate = 1 / totalChapter
  277. const progress = totalChapter ? (rate * (current / total)) + ((p.index - 1) * rate) : 0
  278. return Object.assign({}, p, {total: total, current: current, progress: progress * 100}, p.type == 'text' ? {contents: contents.slice(p.start, p.end).map(char => char.replace(new RegExp(`(${htmlChars.join('|')}):`), ''))} : {})
  279. })
  280. triggerMethod('handleSuccess', pages)
  281. document.querySelector('.yingbing-computed-wrapper').innerHTML = ''
  282. pages = []
  283. startTags = []
  284. contents = []
  285. chapter = {}
  286. }
  287. //销毁处理定时任务
  288. function destroyHandleTimer () {
  289. if ( handleTimer ) {
  290. window.clearTimeout(handleTimer)
  291. handleTimer = null
  292. }
  293. }
  294. function triggerMethod (name, args) {
  295. uni.postMessage({
  296. data: {
  297. [name]: args
  298. }
  299. });
  300. }
  301. </script>
  302. </html>