- 实现EPUB阅读器搜索功能及选中注释功能 - 优化CFI模块,修复代码审查发现的11个问题 - 实现大书远距目录跳转与后台补全优化方案 - 优化设置面板与章节运行时联动 - 重构及大量改进优化
216 lines
9.6 KiB
Swift
216 lines
9.6 KiB
Swift
|
|
import Foundation
|
|
|
|
public enum RDEPUBStyleSheetBuilder {
|
|
|
|
public static func injectPaginationCSS(
|
|
into html: String,
|
|
presentation: RDEPUBPresentationStyle
|
|
) -> String {
|
|
let styleTag = "<style id=\"ss-reader-pagination\">\(measurementCSS(for: presentation))</style>"
|
|
if html.range(of: "</head>", options: .caseInsensitive) != nil {
|
|
return html.replacingOccurrences(of: "</head>", with: styleTag + "</head>", options: .caseInsensitive)
|
|
}
|
|
if html.range(of: "<body", options: .caseInsensitive) != nil {
|
|
return html.replacingOccurrences(of: "<body", with: styleTag + "<body", options: .caseInsensitive)
|
|
}
|
|
return styleTag + html
|
|
}
|
|
|
|
public static func renderCSS(for presentation: RDEPUBPresentationStyle) -> String {
|
|
let values = cssValues(for: presentation)
|
|
let backgroundCSS = presentation.themeBackgroundColor.map { "background: \($0) !important;" } ?? ""
|
|
let textCSS = presentation.themeTextColor.map { "color: \($0) !important;" } ?? ""
|
|
let columnCountCSS = values.numberOfColumns > 1 ? "-webkit-column-count: \(values.numberOfColumns) !important; column-count: \(values.numberOfColumns) !important;" : ""
|
|
|
|
return """
|
|
html {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow-x: hidden !important;
|
|
overflow-y: hidden !important;
|
|
}
|
|
body {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
box-sizing: border-box !important;
|
|
position: relative !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow: hidden !important;
|
|
background: transparent !important;
|
|
}
|
|
#ss-reader-viewport {
|
|
position: relative !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow: hidden !important;
|
|
background: transparent !important;
|
|
}
|
|
#ss-reader-content {
|
|
position: relative !important;
|
|
box-sizing: border-box !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow: visible !important;
|
|
padding: \(values.paddingTop)px \(values.paddingRight)px \(values.paddingBottom)px \(values.paddingLeft)px !important;
|
|
font-size: \(values.fontSize)px !important;
|
|
line-height: \(values.lineHeight) !important;
|
|
-webkit-column-width: \(values.contentWidth)px !important;
|
|
column-width: \(values.contentWidth)px !important;
|
|
\(columnCountCSS)
|
|
-webkit-column-gap: \(values.columnGap)px !important;
|
|
column-gap: \(values.columnGap)px !important;
|
|
-webkit-column-fill: auto !important;
|
|
column-fill: auto !important;
|
|
transform-origin: top left !important;
|
|
will-change: transform !important;
|
|
\(backgroundCSS)
|
|
\(textCSS)
|
|
}
|
|
#ss-reader-content img, #ss-reader-content svg, #ss-reader-content video, #ss-reader-content canvas, #ss-reader-content iframe {
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
break-inside: avoid-column !important;
|
|
-webkit-column-break-inside: avoid !important;
|
|
}
|
|
#ss-reader-content mark.ss-reader-highlight {
|
|
padding: 0;
|
|
border-radius: 2px;
|
|
color: inherit !important;
|
|
}
|
|
#ss-reader-content mark.ss-reader-highlight-highlight {
|
|
text-decoration: none !important;
|
|
color: inherit !important;
|
|
}
|
|
#ss-reader-content mark.ss-reader-highlight-underline {
|
|
background: transparent !important;
|
|
color: inherit !important;
|
|
text-decoration-line: underline !important;
|
|
text-decoration-thickness: 2px !important;
|
|
text-decoration-skip-ink: auto !important;
|
|
}
|
|
"""
|
|
}
|
|
|
|
public static func measurementCSS(for presentation: RDEPUBPresentationStyle) -> String {
|
|
let values = cssValues(for: presentation)
|
|
let backgroundCSS = presentation.themeBackgroundColor.map { "background: \($0) !important;" } ?? ""
|
|
let textCSS = presentation.themeTextColor.map { "color: \($0) !important;" } ?? ""
|
|
let columnCountCSS = values.numberOfColumns > 1 ? "-webkit-column-count: \(values.numberOfColumns) !important; column-count: \(values.numberOfColumns) !important;" : ""
|
|
|
|
return """
|
|
html {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow: hidden !important;
|
|
}
|
|
body {
|
|
margin: 0 !important;
|
|
box-sizing: border-box !important;
|
|
position: relative !important;
|
|
width: \(values.viewportWidth)px !important;
|
|
height: \(values.viewportHeight)px !important;
|
|
min-height: \(values.viewportHeight)px !important;
|
|
overflow: visible !important;
|
|
padding: \(values.paddingTop)px \(values.paddingRight)px \(values.paddingBottom)px \(values.paddingLeft)px !important;
|
|
font-size: \(values.fontSize)px !important;
|
|
line-height: \(values.lineHeight) !important;
|
|
-webkit-column-width: \(values.contentWidth)px !important;
|
|
column-width: \(values.contentWidth)px !important;
|
|
\(columnCountCSS)
|
|
-webkit-column-gap: \(values.columnGap)px !important;
|
|
column-gap: \(values.columnGap)px !important;
|
|
-webkit-column-fill: auto !important;
|
|
column-fill: auto !important;
|
|
transform-origin: top left !important;
|
|
\(backgroundCSS)
|
|
\(textCSS)
|
|
}
|
|
img, svg, video, canvas, iframe {
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
break-inside: avoid-column !important;
|
|
-webkit-column-break-inside: avoid !important;
|
|
}
|
|
"""
|
|
}
|
|
|
|
public static func measurementScript(for presentation: RDEPUBPresentationStyle) -> String {
|
|
let style = measurementCSS(for: presentation)
|
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
.replacingOccurrences(of: "`", with: "\\`")
|
|
|
|
let viewportWidth = Double(max(1, presentation.viewportSize.width))
|
|
let viewportWidthJS = String(format: "%.3f", viewportWidth)
|
|
|
|
return """
|
|
(function() {
|
|
var style = document.getElementById('ss-reader-pagination');
|
|
if (!style) {
|
|
style = document.createElement('style');
|
|
style.id = 'ss-reader-pagination';
|
|
document.head.appendChild(style);
|
|
}
|
|
style.textContent = `\(style)`;
|
|
var scrollingElement = document.scrollingElement || document.documentElement || document.body;
|
|
var totalWidth = Math.max(
|
|
scrollingElement ? scrollingElement.scrollWidth : 0,
|
|
scrollingElement ? scrollingElement.offsetWidth : 0,
|
|
document.body ? document.body.scrollWidth : 0,
|
|
document.body ? document.body.offsetWidth : 0,
|
|
document.documentElement ? document.documentElement.scrollWidth : 0,
|
|
document.documentElement ? document.documentElement.offsetWidth : 0
|
|
);
|
|
return Math.max(1, Math.ceil(totalWidth / \(viewportWidthJS)));
|
|
})();
|
|
"""
|
|
}
|
|
|
|
private static func cssValues(for presentation: RDEPUBPresentationStyle) -> (
|
|
viewportWidth: String,
|
|
viewportHeight: String,
|
|
contentWidth: String,
|
|
columnGap: String,
|
|
numberOfColumns: Int,
|
|
paddingTop: String,
|
|
paddingRight: String,
|
|
paddingBottom: String,
|
|
paddingLeft: String,
|
|
fontSize: String,
|
|
lineHeight: String
|
|
) {
|
|
let viewportWidth = max(1, presentation.viewportSize.width)
|
|
let viewportHeight = max(1, presentation.viewportSize.height)
|
|
let contentInsets = presentation.contentInsets
|
|
let numberOfColumns = max(1, presentation.numberOfColumns)
|
|
let availableWidth = max(1, viewportWidth - contentInsets.left - contentInsets.right)
|
|
let totalGap = CGFloat(numberOfColumns - 1) * max(0, presentation.columnGap)
|
|
let contentWidth = max(1, (availableWidth - totalGap) / CGFloat(numberOfColumns))
|
|
let columnGap = max(0, presentation.columnGap)
|
|
|
|
return (
|
|
viewportWidth: String(format: "%.3f", viewportWidth),
|
|
viewportHeight: String(format: "%.3f", viewportHeight),
|
|
contentWidth: String(format: "%.3f", contentWidth),
|
|
columnGap: String(format: "%.3f", columnGap),
|
|
numberOfColumns: numberOfColumns,
|
|
paddingTop: String(format: "%.3f", contentInsets.top),
|
|
paddingRight: String(format: "%.3f", contentInsets.right),
|
|
paddingBottom: String(format: "%.3f", contentInsets.bottom),
|
|
paddingLeft: String(format: "%.3f", contentInsets.left),
|
|
fontSize: String(format: "%.3f", presentation.fontSize),
|
|
lineHeight: String(format: "%.3f", max(1, presentation.lineHeightMultiple))
|
|
)
|
|
}
|
|
}
|