import Foundation public enum RDEPUBStyleSheetBuilder { public static func injectPaginationCSS( into html: String, presentation: RDEPUBPresentationStyle ) -> String { let styleTag = "" if html.range(of: "", options: .caseInsensitive) != nil { return html.replacingOccurrences(of: "", with: styleTag + "", options: .caseInsensitive) } if html.range(of: " 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)) ) } }