SimonFairbairn / SwiftyMarkdown

Converts Markdown files and strings into NSAttributedStrings with lots of customisation options.
MIT License
1.63k stars 269 forks source link

Idea: Include means for Markdown styles to be defined flexibly, strongly-typed #153

Open akingdom opened 2 months ago

akingdom commented 2 months ago

I needed to patch SwiftyMarkdown into existing code. Styles needed to be compatible, strongly typed, consistent and readable. Here's my example of usage. Hope it helps someone.

Detail:

init() takes a list of markdown styles such as body and applies StyleProperty to them. func applyStyles copies the custom properties to the styles. enum StyleProperty provides the custom properties structure.

Code:

import AppKit
import SwiftUI
import SwiftyMarkdown

class MarkdownHelper {
    static var shared: MarkdownHelper = MarkdownHelper()
    static let customFontName = "Bitstream Vera Sans Mono"
    var md : SwiftyMarkdown

    private init() {
        let md : SwiftyMarkdown = SwiftyMarkdown(string: "")
        self.md = md
        let size = 14
        applyStyles(stylesMapping: [

            md.body: [  // the default style
                .fontName(customFontName),
                .fontStyle(.normal),
                .fontSize(size),
                .color(PhosphorColors.white)
            ],
            md.code: [  // the code style
                .fontName(customFontName),
                .fontStyle(.normal),
                .fontSize(size),
                .color(PhosphorColors.white),
            ],
            md.h1: [
                .fontName(customFontName),
                .fontStyle(.bold),
                .fontSize(size),
                .color(PhosphorColors.brightWhite)
            ],
            md.h2: [
                .fontName(customFontName),
                .fontStyle(.bold),
                .fontSize(size),
                .color(PhosphorColors.brightAmber)
            ],
            md.h3: [
                .fontName(customFontName),
                .fontStyle(.bold),
                .fontSize(size),
                .color(PhosphorColors.brightCyan)
            ],
            md.italic: [
                .fontName(customFontName),
                .fontStyle(.italic),
                .fontSize(size),
                .color(PhosphorColors.green)
            ],
            md.bold: [
                .fontName(customFontName),
                .fontStyle(.bold),
                .fontSize(size),
                .color(PhosphorColors.overBrightGreen)
            ]
        ])
    }

    enum StyleProperty {
        case fontName(String)
        case color(NSColor)
        case fontSize(CGFloat)
        case fontStyle(FontStyle)
        case alignment(NSTextAlignment)
        case lineSpacing(CGFloat)
        case paragraphSpacing(CGFloat)
        case underlineStyle(NSUnderlineStyle)
        case underlineColor(NSColor)
    }

    func applyStyles(stylesMapping: [NSObject: [StyleProperty]]) {
        // Applying styles
        for (instance, properties) in stylesMapping {
            for property in properties {
                switch property {
                case .fontName(let value):
                    (instance as? BasicStyles)?.fontName = value
                    (instance as? LineStyles)?.fontName = value
                case .color(let value):
                    (instance as? BasicStyles)?.color = value
                    (instance as? LineStyles)?.color = value
                case .fontSize(let value):
                    (instance as? BasicStyles)?.fontSize = value
                    (instance as? LineStyles)?.fontSize = value
                case .fontStyle(let value):
                    (instance as? BasicStyles)?.fontStyle = value
                    (instance as? LineStyles)?.fontStyle = value
                case .alignment(let value):
                    (instance as? LineStyles)?.alignment = value
                case .lineSpacing(let value):
                    (instance as? LineStyles)?.lineSpacing = value
                case .paragraphSpacing(let value):
                    (instance as? LineStyles)?.paragraphSpacing = value
                case .underlineStyle(let value):
                    (instance as? LinkStyles)?.underlineStyle = value
                case .underlineColor(let value):
                    (instance as? LinkStyles)?.underlineColor = value
                }
            }
        }
    }

    func parse(markdown: String) -> NSAttributedString {
        md.string = markdown.replacingOccurrences(of: "\r\n", with: "\n")  // the presumption is that styles do not change, otherwise we'd reinitialise all styles each time.
        return md.attributedString()
    }

    struct PhosphorColors {
        static let white : NSColor = NSColor(Color(#colorLiteral(red: 0.9159484512, green: 0.9159484512, blue: 0.9463420244, alpha: 1)))
        static let amber : NSColor = NSColor(Color(#colorLiteral(red: 0.990, green: 0.857, blue: 0.205, alpha: 1)))
        static let darkAmber : NSColor = NSColor(Color(#colorLiteral(red: 0.937, green: 0.622, blue: 0.227, alpha: 1)))
        static let mediumBrightBlue : NSColor = NSColor(Color(#colorLiteral(red: 0.699, green: 0.834, blue: 0.929, alpha: 1)))
        static let blue : NSColor = NSColor(Color(#colorLiteral(red: 0.623, green: 0.826, blue: 0.973, alpha: 1)))
        static let darkBlue : NSColor = NSColor(Color(#colorLiteral(red: 0.343, green: 0.559, blue: 0.731, alpha: 1)))
        static let overBrightGreen : NSColor = NSColor(Color(#colorLiteral(red: 0.596, green: 0.991, blue: 0.988, alpha: 1)))
        static let darkGreen : NSColor = NSColor(Color(#colorLiteral(red: 0.400, green: 0.704, blue: 0.386, alpha: 1)))
        static let green : NSColor = NSColor(Color(#colorLiteral(red: 0.231, green: 0.925, blue: 0.349, alpha: 1)))
        static let noSignal : NSColor = NSColor(Color(#colorLiteral(red: 0.017, green: 0.198, blue: 1.000, alpha: 1)))
        static let red : NSColor = NSColor(Color(#colorLiteral(red: 0.940, green: 0.300, blue: 0.280, alpha: 1)))
        static let magenta : NSColor = NSColor(Color(#colorLiteral(red: 0.780, green: 0.360, blue: 0.780, alpha: 1)))
        static let cyan : NSColor = NSColor(Color(#colorLiteral(red: 0.420, green: 0.840, blue: 0.780, alpha: 1)))
        static let yellow : NSColor = NSColor(Color(#colorLiteral(red: 0.980, green: 0.880, blue: 0.240, alpha: 1)))
        // Bright colors based on your existing colors
        static let brightWhite : NSColor = NSColor(Color(#colorLiteral(red: 1.000, green: 1.000, blue: 1.000, alpha: 1)))
        static let brightAmber : NSColor = NSColor(Color(#colorLiteral(red: 1.000, green: 0.922, blue: 0.531, alpha: 1)))
        static let brightBrightBlue : NSColor = NSColor(Color(#colorLiteral(red: 0.792, green: 0.937, blue: 1.000, alpha: 1)))
        static let brightBlue : NSColor = NSColor(Color(#colorLiteral(red: 0.792, green: 0.923, blue: 1.000, alpha: 1)))
        static let brightDarkBlue : NSColor = NSColor(Color(#colorLiteral(red: 0.517, green: 0.735, blue: 1.000, alpha: 1)))
        static let brightOverBrightGreen : NSColor = NSColor(Color(#colorLiteral(red: 0.800, green: 1.000, blue: 1.000, alpha: 1)))
        static let brightDarkGreen : NSColor = NSColor(Color(#colorLiteral(red: 0.543, green: 0.893, blue: 0.756, alpha: 1)))
        static let brightGreen : NSColor = NSColor(Color(#colorLiteral(red: 0.447, green: 1.000, blue: 0.752, alpha: 1)))
        static let brightNoSignal : NSColor = NSColor(Color(#colorLiteral(red: 0.042, green: 0.556, blue: 1.000, alpha: 1)))
        static let brightRed : NSColor = NSColor(Color(#colorLiteral(red: 1.000, green: 0.440, blue: 0.420, alpha: 1)))
        static let brightMagenta : NSColor = NSColor(Color(#colorLiteral(red: 0.896, green: 0.501, blue: 0.896, alpha: 1)))
        static let brightCyan : NSColor = NSColor(Color(#colorLiteral(red: 0.504, green: 0.954, blue: 0.934, alpha: 1)))
        static let brightYellow : NSColor = NSColor(Color(#colorLiteral(red: 1.000, green: 0.930, blue: 0.300, alpha: 1)))
    }

}

extension Color {
    init(_ nsColor: NSColor) {
        self.init(
            red: Double(nsColor.redComponent),
            green: Double(nsColor.greenComponent),
            blue: Double(nsColor.blueComponent),
            opacity: Double(nsColor.alphaComponent)
        )
    }
}

extension NSColor {
    var asColor: Color {
        let r = Double(redComponent)
        let g = Double(greenComponent)
        let b = Double(blueComponent)
        let a = Double(alphaComponent)
        return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
    }
}

Caveat: original code works fine but I've removed various code unneeded for the example.