├── README.md
├── SwiftUIRenderer.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcuserdata
│ └── lukasmoller.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
└── SwiftUIRenderer
├── AppDelegate.swift
├── Assets.xcassets
├── AppIcon.appiconset
│ └── Contents.json
└── Contents.json
├── AttributedText.swift
├── Base.lproj
└── Main.storyboard
├── ContentView.swift
├── Info.plist
├── Preview Content
└── Preview Assets.xcassets
│ └── Contents.json
└── SwiftUIRenderer.entitlements
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI-Formatted-Text
2 | A simple proof-of-concept SwiftUI application that renders a HTML-like language using SwiftUI Text elements.
3 |
4 | ## Description
5 | Goal of this project is to test the described proof-of-concept. In the moment both the parser and the renderer are fairly
6 | buggy. The language used is closely related to HTML and those comfortable with HTML should also be comfortable with this
7 | language. The string is parsed into an abstract sytnax tree consisting of `Tag` structs. The tree is then rendered to
8 | SwiftUI native `Text` views.
9 |
10 | ## Use Cases
11 | - Formatted localized strings
12 | - Loading text from a database
13 | - Allowing the user to format text in a certain way
14 |
15 | ## Syntax
16 | ### Tags
17 | The following tags are implemented:
18 | - `largeTitle` / `h1`
19 | - `title` / `h2`
20 | - `headline` / `h3`
21 | - `subheadline` / `h4`
22 | - `body`
23 | - `callout` / `h5`
24 | - `caption` / `h6`
25 | - `footnote`
26 | - `b`
27 | - `i`
28 | - `u`
29 | - `br`
30 | - `font`
31 | - Attributes:
32 | - `family`
33 | - `size`
34 | - `color`
35 | - `family` and `size` attributes both have to be present for them to have any effect
36 | - `color` can only be given using hex values i.e. `#ff0000` and `#ff0000aa`
37 |
38 | Block elements like h1/h2/h3/h4/h5 are not implemented in the moment. Newlines can only be added using `\n` and `
`
39 |
40 | The XML-like parser is very rudimentary and does not follow any specs. Unlike in HTML every tag that was opened has to be
41 | closed - this includes `
`.
42 | ### 👉 This is just a proof-of-concept that should not be used in any application
43 |
--------------------------------------------------------------------------------
/SwiftUIRenderer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8C5A856622BA3D4500854D5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5A856522BA3D4500854D5C /* AppDelegate.swift */; };
11 | 8C5A856822BA3D4500854D5C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5A856722BA3D4500854D5C /* ContentView.swift */; };
12 | 8C5A856A22BA3D4700854D5C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C5A856922BA3D4700854D5C /* Assets.xcassets */; };
13 | 8C5A856D22BA3D4700854D5C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C5A856C22BA3D4700854D5C /* Preview Assets.xcassets */; };
14 | 8C5A857022BA3D4700854D5C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8C5A856E22BA3D4700854D5C /* Main.storyboard */; };
15 | 8C5A857922BA3D5F00854D5C /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5A857822BA3D5F00854D5C /* AttributedText.swift */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | 8C5A856222BA3D4500854D5C /* SwiftUIRenderer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIRenderer.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 8C5A856522BA3D4500854D5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
21 | 8C5A856722BA3D4500854D5C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | 8C5A856922BA3D4700854D5C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | 8C5A856C22BA3D4700854D5C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
24 | 8C5A856F22BA3D4700854D5C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
25 | 8C5A857122BA3D4700854D5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
26 | 8C5A857222BA3D4700854D5C /* SwiftUIRenderer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftUIRenderer.entitlements; sourceTree = ""; };
27 | 8C5A857822BA3D5F00854D5C /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = ""; };
28 | /* End PBXFileReference section */
29 |
30 | /* Begin PBXFrameworksBuildPhase section */
31 | 8C5A855F22BA3D4500854D5C /* Frameworks */ = {
32 | isa = PBXFrameworksBuildPhase;
33 | buildActionMask = 2147483647;
34 | files = (
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | 8C5A855922BA3D4500854D5C = {
42 | isa = PBXGroup;
43 | children = (
44 | 8C5A856422BA3D4500854D5C /* SwiftUIRenderer */,
45 | 8C5A856322BA3D4500854D5C /* Products */,
46 | );
47 | sourceTree = "";
48 | };
49 | 8C5A856322BA3D4500854D5C /* Products */ = {
50 | isa = PBXGroup;
51 | children = (
52 | 8C5A856222BA3D4500854D5C /* SwiftUIRenderer.app */,
53 | );
54 | name = Products;
55 | sourceTree = "";
56 | };
57 | 8C5A856422BA3D4500854D5C /* SwiftUIRenderer */ = {
58 | isa = PBXGroup;
59 | children = (
60 | 8C5A856522BA3D4500854D5C /* AppDelegate.swift */,
61 | 8C5A856722BA3D4500854D5C /* ContentView.swift */,
62 | 8C5A857822BA3D5F00854D5C /* AttributedText.swift */,
63 | 8C5A856922BA3D4700854D5C /* Assets.xcassets */,
64 | 8C5A856E22BA3D4700854D5C /* Main.storyboard */,
65 | 8C5A857122BA3D4700854D5C /* Info.plist */,
66 | 8C5A857222BA3D4700854D5C /* SwiftUIRenderer.entitlements */,
67 | 8C5A856B22BA3D4700854D5C /* Preview Content */,
68 | );
69 | path = SwiftUIRenderer;
70 | sourceTree = "";
71 | };
72 | 8C5A856B22BA3D4700854D5C /* Preview Content */ = {
73 | isa = PBXGroup;
74 | children = (
75 | 8C5A856C22BA3D4700854D5C /* Preview Assets.xcassets */,
76 | );
77 | path = "Preview Content";
78 | sourceTree = "";
79 | };
80 | /* End PBXGroup section */
81 |
82 | /* Begin PBXNativeTarget section */
83 | 8C5A856122BA3D4500854D5C /* SwiftUIRenderer */ = {
84 | isa = PBXNativeTarget;
85 | buildConfigurationList = 8C5A857522BA3D4700854D5C /* Build configuration list for PBXNativeTarget "SwiftUIRenderer" */;
86 | buildPhases = (
87 | 8C5A855E22BA3D4500854D5C /* Sources */,
88 | 8C5A855F22BA3D4500854D5C /* Frameworks */,
89 | 8C5A856022BA3D4500854D5C /* Resources */,
90 | );
91 | buildRules = (
92 | );
93 | dependencies = (
94 | );
95 | name = SwiftUIRenderer;
96 | productName = SwiftUIRenderer;
97 | productReference = 8C5A856222BA3D4500854D5C /* SwiftUIRenderer.app */;
98 | productType = "com.apple.product-type.application";
99 | };
100 | /* End PBXNativeTarget section */
101 |
102 | /* Begin PBXProject section */
103 | 8C5A855A22BA3D4500854D5C /* Project object */ = {
104 | isa = PBXProject;
105 | attributes = {
106 | LastSwiftUpdateCheck = 1100;
107 | LastUpgradeCheck = 1100;
108 | ORGANIZATIONNAME = "Lukas Möller";
109 | TargetAttributes = {
110 | 8C5A856122BA3D4500854D5C = {
111 | CreatedOnToolsVersion = 11.0;
112 | };
113 | };
114 | };
115 | buildConfigurationList = 8C5A855D22BA3D4500854D5C /* Build configuration list for PBXProject "SwiftUIRenderer" */;
116 | compatibilityVersion = "Xcode 9.3";
117 | developmentRegion = en;
118 | hasScannedForEncodings = 0;
119 | knownRegions = (
120 | en,
121 | Base,
122 | );
123 | mainGroup = 8C5A855922BA3D4500854D5C;
124 | productRefGroup = 8C5A856322BA3D4500854D5C /* Products */;
125 | projectDirPath = "";
126 | projectRoot = "";
127 | targets = (
128 | 8C5A856122BA3D4500854D5C /* SwiftUIRenderer */,
129 | );
130 | };
131 | /* End PBXProject section */
132 |
133 | /* Begin PBXResourcesBuildPhase section */
134 | 8C5A856022BA3D4500854D5C /* Resources */ = {
135 | isa = PBXResourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | 8C5A857022BA3D4700854D5C /* Main.storyboard in Resources */,
139 | 8C5A856D22BA3D4700854D5C /* Preview Assets.xcassets in Resources */,
140 | 8C5A856A22BA3D4700854D5C /* Assets.xcassets in Resources */,
141 | );
142 | runOnlyForDeploymentPostprocessing = 0;
143 | };
144 | /* End PBXResourcesBuildPhase section */
145 |
146 | /* Begin PBXSourcesBuildPhase section */
147 | 8C5A855E22BA3D4500854D5C /* Sources */ = {
148 | isa = PBXSourcesBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | 8C5A857922BA3D5F00854D5C /* AttributedText.swift in Sources */,
152 | 8C5A856822BA3D4500854D5C /* ContentView.swift in Sources */,
153 | 8C5A856622BA3D4500854D5C /* AppDelegate.swift in Sources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXSourcesBuildPhase section */
158 |
159 | /* Begin PBXVariantGroup section */
160 | 8C5A856E22BA3D4700854D5C /* Main.storyboard */ = {
161 | isa = PBXVariantGroup;
162 | children = (
163 | 8C5A856F22BA3D4700854D5C /* Base */,
164 | );
165 | name = Main.storyboard;
166 | sourceTree = "";
167 | };
168 | /* End PBXVariantGroup section */
169 |
170 | /* Begin XCBuildConfiguration section */
171 | 8C5A857322BA3D4700854D5C /* Debug */ = {
172 | isa = XCBuildConfiguration;
173 | buildSettings = {
174 | ALWAYS_SEARCH_USER_PATHS = NO;
175 | CLANG_ANALYZER_NONNULL = YES;
176 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
177 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
178 | CLANG_CXX_LIBRARY = "libc++";
179 | CLANG_ENABLE_MODULES = YES;
180 | CLANG_ENABLE_OBJC_ARC = YES;
181 | CLANG_ENABLE_OBJC_WEAK = YES;
182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
183 | CLANG_WARN_BOOL_CONVERSION = YES;
184 | CLANG_WARN_COMMA = YES;
185 | CLANG_WARN_CONSTANT_CONVERSION = YES;
186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
189 | CLANG_WARN_EMPTY_BODY = YES;
190 | CLANG_WARN_ENUM_CONVERSION = YES;
191 | CLANG_WARN_INFINITE_RECURSION = YES;
192 | CLANG_WARN_INT_CONVERSION = YES;
193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
197 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
198 | CLANG_WARN_STRICT_PROTOTYPES = YES;
199 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
200 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
201 | CLANG_WARN_UNREACHABLE_CODE = YES;
202 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = dwarf;
205 | ENABLE_STRICT_OBJC_MSGSEND = YES;
206 | ENABLE_TESTABILITY = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu11;
208 | GCC_DYNAMIC_NO_PIC = NO;
209 | GCC_NO_COMMON_BLOCKS = YES;
210 | GCC_OPTIMIZATION_LEVEL = 0;
211 | GCC_PREPROCESSOR_DEFINITIONS = (
212 | "DEBUG=1",
213 | "$(inherited)",
214 | );
215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
217 | GCC_WARN_UNDECLARED_SELECTOR = YES;
218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
219 | GCC_WARN_UNUSED_FUNCTION = YES;
220 | GCC_WARN_UNUSED_VARIABLE = YES;
221 | MACOSX_DEPLOYMENT_TARGET = 10.15;
222 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
223 | MTL_FAST_MATH = YES;
224 | ONLY_ACTIVE_ARCH = YES;
225 | SDKROOT = macosx;
226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
228 | };
229 | name = Debug;
230 | };
231 | 8C5A857422BA3D4700854D5C /* Release */ = {
232 | isa = XCBuildConfiguration;
233 | buildSettings = {
234 | ALWAYS_SEARCH_USER_PATHS = NO;
235 | CLANG_ANALYZER_NONNULL = YES;
236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
238 | CLANG_CXX_LIBRARY = "libc++";
239 | CLANG_ENABLE_MODULES = YES;
240 | CLANG_ENABLE_OBJC_ARC = YES;
241 | CLANG_ENABLE_OBJC_WEAK = YES;
242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
243 | CLANG_WARN_BOOL_CONVERSION = YES;
244 | CLANG_WARN_COMMA = YES;
245 | CLANG_WARN_CONSTANT_CONVERSION = YES;
246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
249 | CLANG_WARN_EMPTY_BODY = YES;
250 | CLANG_WARN_ENUM_CONVERSION = YES;
251 | CLANG_WARN_INFINITE_RECURSION = YES;
252 | CLANG_WARN_INT_CONVERSION = YES;
253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
258 | CLANG_WARN_STRICT_PROTOTYPES = YES;
259 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
261 | CLANG_WARN_UNREACHABLE_CODE = YES;
262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
263 | COPY_PHASE_STRIP = NO;
264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
265 | ENABLE_NS_ASSERTIONS = NO;
266 | ENABLE_STRICT_OBJC_MSGSEND = YES;
267 | GCC_C_LANGUAGE_STANDARD = gnu11;
268 | GCC_NO_COMMON_BLOCKS = YES;
269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
271 | GCC_WARN_UNDECLARED_SELECTOR = YES;
272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
273 | GCC_WARN_UNUSED_FUNCTION = YES;
274 | GCC_WARN_UNUSED_VARIABLE = YES;
275 | MACOSX_DEPLOYMENT_TARGET = 10.15;
276 | MTL_ENABLE_DEBUG_INFO = NO;
277 | MTL_FAST_MATH = YES;
278 | SDKROOT = macosx;
279 | SWIFT_COMPILATION_MODE = wholemodule;
280 | SWIFT_OPTIMIZATION_LEVEL = "-O";
281 | };
282 | name = Release;
283 | };
284 | 8C5A857622BA3D4700854D5C /* Debug */ = {
285 | isa = XCBuildConfiguration;
286 | buildSettings = {
287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
288 | CODE_SIGN_ENTITLEMENTS = SwiftUIRenderer/SwiftUIRenderer.entitlements;
289 | CODE_SIGN_STYLE = Automatic;
290 | COMBINE_HIDPI_IMAGES = YES;
291 | DEVELOPMENT_ASSET_PATHS = "SwiftUIRenderer/Preview\\ Content";
292 | DEVELOPMENT_TEAM = H4Q4MV9KW7;
293 | ENABLE_HARDENED_RUNTIME = YES;
294 | ENABLE_PREVIEWS = YES;
295 | INFOPLIST_FILE = SwiftUIRenderer/Info.plist;
296 | LD_RUNPATH_SEARCH_PATHS = (
297 | "$(inherited)",
298 | "@executable_path/../Frameworks",
299 | );
300 | PRODUCT_BUNDLE_IDENTIFIER = moeller.lukas.SwiftUIRenderer;
301 | PRODUCT_NAME = "$(TARGET_NAME)";
302 | SWIFT_VERSION = 5.0;
303 | };
304 | name = Debug;
305 | };
306 | 8C5A857722BA3D4700854D5C /* Release */ = {
307 | isa = XCBuildConfiguration;
308 | buildSettings = {
309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
310 | CODE_SIGN_ENTITLEMENTS = SwiftUIRenderer/SwiftUIRenderer.entitlements;
311 | CODE_SIGN_STYLE = Automatic;
312 | COMBINE_HIDPI_IMAGES = YES;
313 | DEVELOPMENT_ASSET_PATHS = "SwiftUIRenderer/Preview\\ Content";
314 | DEVELOPMENT_TEAM = H4Q4MV9KW7;
315 | ENABLE_HARDENED_RUNTIME = YES;
316 | ENABLE_PREVIEWS = YES;
317 | INFOPLIST_FILE = SwiftUIRenderer/Info.plist;
318 | LD_RUNPATH_SEARCH_PATHS = (
319 | "$(inherited)",
320 | "@executable_path/../Frameworks",
321 | );
322 | PRODUCT_BUNDLE_IDENTIFIER = moeller.lukas.SwiftUIRenderer;
323 | PRODUCT_NAME = "$(TARGET_NAME)";
324 | SWIFT_VERSION = 5.0;
325 | };
326 | name = Release;
327 | };
328 | /* End XCBuildConfiguration section */
329 |
330 | /* Begin XCConfigurationList section */
331 | 8C5A855D22BA3D4500854D5C /* Build configuration list for PBXProject "SwiftUIRenderer" */ = {
332 | isa = XCConfigurationList;
333 | buildConfigurations = (
334 | 8C5A857322BA3D4700854D5C /* Debug */,
335 | 8C5A857422BA3D4700854D5C /* Release */,
336 | );
337 | defaultConfigurationIsVisible = 0;
338 | defaultConfigurationName = Release;
339 | };
340 | 8C5A857522BA3D4700854D5C /* Build configuration list for PBXNativeTarget "SwiftUIRenderer" */ = {
341 | isa = XCConfigurationList;
342 | buildConfigurations = (
343 | 8C5A857622BA3D4700854D5C /* Debug */,
344 | 8C5A857722BA3D4700854D5C /* Release */,
345 | );
346 | defaultConfigurationIsVisible = 0;
347 | defaultConfigurationName = Release;
348 | };
349 | /* End XCConfigurationList section */
350 | };
351 | rootObject = 8C5A855A22BA3D4500854D5C /* Project object */;
352 | }
353 |
--------------------------------------------------------------------------------
/SwiftUIRenderer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUIRenderer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftUIRenderer.xcodeproj/xcuserdata/lukasmoller.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/SwiftUIRenderer.xcodeproj/xcuserdata/lukasmoller.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SwiftUIRenderer.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftUIRenderer
4 | //
5 | // Created by Lukas Möller on 19.06.19.
6 | // Copyright © 2019 Lukas Möller. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftUI
11 |
12 | @NSApplicationMain
13 | class AppDelegate: NSObject, NSApplicationDelegate {
14 |
15 | var window: NSWindow!
16 |
17 | func applicationDidFinishLaunching(_ aNotification: Notification) {
18 | // Insert code here to initialize your application
19 | window = NSWindow(
20 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
21 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
22 | backing: .buffered, defer: false)
23 | window.center()
24 | window.setFrameAutosaveName("Main Window")
25 |
26 | window.contentView = NSHostingView(rootView: ContentView())
27 |
28 | window.makeKeyAndOrderFront(nil)
29 | }
30 |
31 | func applicationWillTerminate(_ aNotification: Notification) {
32 | // Insert code here to tear down your application
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/SwiftUIRenderer/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUIRenderer/AttributedText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AttributedText.swift
3 | // SwiftUIRenderer
4 | //
5 | // Created by Lukas Möller on 19.06.19.
6 | // Copyright © 2019 Lukas Möller. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | struct AttributedStringStyle {
11 | let font: Font?
12 | let weight: Font.Weight?
13 | let color: Color?
14 | let italic: Bool?
15 | let underline: Bool?
16 | let block: Bool?
17 | init(font: Font? = nil, weight: Font.Weight? = nil, color: Color? = nil, italic: Bool? = nil, underline: Bool? = nil, block: Bool? = nil) {
18 | self.font = font
19 | self.weight = weight
20 | self.color = color
21 | self.italic = italic
22 | self.underline = underline
23 | self.block = block
24 | }
25 | static var `default`: AttributedStringStyle {
26 | return AttributedStringStyle(font: nil, weight: nil, color: nil, italic: nil, underline: nil, block: nil)
27 | }
28 | func applying(partial: AttributedStringStyle) -> AttributedStringStyle{
29 | let font = partial.font ?? self.font
30 | let weight = partial.weight ?? self.weight
31 | let color = partial.color ?? self.color
32 | let italic = partial.italic ?? self.italic
33 | let underline = partial.underline ?? self.underline
34 | let block = partial.block ?? self.block
35 | return AttributedStringStyle(font: font, weight: weight, color: color, italic: italic, underline: underline, block: block)
36 | }
37 | func childVersion() -> AttributedStringStyle {
38 | let font = self.font
39 | let weight = self.weight
40 | let color = self.color
41 | let italic = self.italic
42 | let underline = self.underline
43 | let block = false
44 | return AttributedStringStyle(font: font, weight: weight, color: color, italic: italic, underline: underline, block: block)
45 | }
46 | }
47 | extension AttributedStringStyle: Equatable {
48 |
49 | }
50 | indirect enum Tag {
51 | case array([Tag])
52 | case font(style: AttributedStringStyle, child: Tag)
53 | case text(String)
54 | case newline
55 | func render(parentStyle: AttributedStringStyle? = nil) -> Text {
56 | let style = parentStyle ?? AttributedStringStyle.default
57 | switch self {
58 | case .array(let tags):
59 | return tags.map({$0.render(parentStyle: style)}).reduce(Text(""), +)
60 | case .font(let partial, let children):
61 | let newStyle = style.applying(partial: partial)
62 | let result = children.render(parentStyle: newStyle)
63 | //TODO: Properly handle block styling
64 | if let block = newStyle.block, block {
65 | return result
66 | } else {
67 | return result
68 | }
69 | case .text(let string):
70 | let components = string.components(separatedBy: NSCharacterSet.whitespacesAndNewlines)
71 | let sanitizedString = components.filter { !$0.isEmpty }.joined(separator: " ") + " "
72 | var node = Text(sanitizedString)
73 | var font: Font = style.font ?? .body
74 | if let italic = style.italic, italic {
75 | font = font.italic()
76 | }
77 | if let underline = style.underline, underline {
78 | node = node.underline()
79 | }
80 |
81 | node = node.font(font)
82 | if let fontWeight = style.weight {
83 | node = node.fontWeight(fontWeight)
84 | }
85 | if let color = style.color {
86 | node = node.foregroundColor(color)
87 | }
88 | return node
89 | case .newline:
90 | return Text("\n")
91 | }
92 | }
93 | }
94 | extension Tag: Equatable {
95 | static func == (lhs: Tag, rhs: Tag) -> Bool {
96 | switch (lhs, rhs) {
97 | case (.array(let lhs), .array(let rhs)):
98 | return lhs == rhs
99 | case (.font(let lhsStyle, let lhsChild), .font(let rhsStyle, let rhsChild)):
100 | return lhsStyle == rhsStyle && lhsChild == rhsChild
101 | case (.text(let lhs), .text(let rhs)):
102 | return lhs == rhs
103 | case (.newline, .newline):
104 | return true
105 | default:
106 | return false
107 | }
108 | }
109 | }
110 | extension Tag {
111 | static func parse(from string: String) -> Tag {
112 | let entityMap: [String: String] = [
113 | "lt;": "<",
114 | "gt;": ">"
115 | ]
116 | let ws: Set = [" "]
117 | var index = string.startIndex
118 | func advance() {
119 | if eof() {
120 | return
121 | }
122 | index = string.index(after: index)
123 | }
124 | func current()-> Character {
125 | return string[index]
126 | }
127 | func peek()-> Character {
128 | return string[string.index(after: index)]
129 | }
130 | func eof() -> Bool {
131 | return index >= string.endIndex
132 | }
133 | func peekIsEof() -> Bool {
134 | return string.index(after: index) >= string.endIndex
135 | }
136 | func skipWhiteSpace() {
137 | while !eof() && ws.contains(current()) {
138 | advance()
139 | }
140 | }
141 | func parseEnclosedString() -> String? {
142 | guard !eof() && current() == "\"" else {
143 | return nil
144 | }
145 | advance()
146 | var buffer = ""
147 | while !eof() && current() != "\"" {
148 | buffer.append(current())
149 | advance()
150 | }
151 | guard !eof() && current() == "\"" else {
152 | return nil
153 | }
154 | advance()
155 | return buffer
156 | }
157 | func parseTag() -> Tag? {
158 | guard !eof() && current() == "<" else {
159 | return nil
160 | }
161 | advance()
162 | var tagName = ""
163 | var attributes: [String: String] = [:]
164 | while !eof() && !current().isWhitespace && current() != "/" && current() != ">"{
165 | tagName.append(current())
166 | advance()
167 | }
168 | //Parsing Attributes
169 | skipWhiteSpace()
170 | while !eof() && current() != ">" && current() != "/" {
171 | var attributeName = ""
172 | while !eof() && !current().isWhitespace && current() != "="{
173 | attributeName.append(current())
174 | advance()
175 | }
176 | advance()
177 | guard let value = parseEnclosedString() else {
178 | return nil
179 | }
180 | attributes[attributeName] = value
181 | skipWhiteSpace()
182 | }
183 | var content: Tag? = nil
184 | if !eof() && current() == "/" {
185 | advance()
186 | guard !eof() && current() == ">" else {
187 | return nil
188 | }
189 | advance()
190 | } else {
191 | guard !eof() && current() == ">" else {
192 | return nil
193 | }
194 | advance()
195 |
196 | content = parse()
197 |
198 | guard !eof() && current() == "<" else {
199 | return nil
200 | }
201 | advance()
202 | guard !eof() && current() == "/" else {
203 | return nil
204 | }
205 | advance()
206 | var closingtagName: String = ""
207 | while !eof() && !current().isWhitespace && current() != "/" && current() != ">"{
208 | closingtagName.append(current())
209 | advance()
210 | }
211 | guard closingtagName == tagName else {
212 | return nil
213 | }
214 | guard !eof() && current() == ">" else {
215 | return nil
216 | }
217 | advance()
218 | }
219 |
220 | switch tagName {
221 | case "largeTitle", "h1":
222 | guard let content = content else {
223 | return nil
224 | }
225 | let style = AttributedStringStyle(font: .largeTitle, block: true)
226 | return .font(style: style, child: content)
227 | case "title", "h2":
228 | guard let content = content else {
229 | return nil
230 | }
231 | let style = AttributedStringStyle(font: .title, block: true)
232 | return .font(style: style, child: content)
233 | case "headline", "h3":
234 | guard let content = content else {
235 | return nil
236 | }
237 | let style = AttributedStringStyle(font: .headline, block: true)
238 | return .font(style: style, child: content)
239 | case "subheadline", "h4":
240 | guard let content = content else {
241 | return nil
242 | }
243 | let style = AttributedStringStyle(font: .subheadline, block: true)
244 | return .font(style: style, child: content)
245 | case "body":
246 | guard let content = content else {
247 | return nil
248 | }
249 | let style = AttributedStringStyle(font: .body)
250 | return .font(style: style, child: content)
251 | case "callout", "h5":
252 | guard let content = content else {
253 | return nil
254 | }
255 | let style = AttributedStringStyle(font: .callout, block: true)
256 | return .font(style: style, child: content)
257 | case "caption", "h6":
258 | guard let content = content else {
259 | return nil
260 | }
261 | let style = AttributedStringStyle(font: .caption, block: true)
262 | return .font(style: style, child: content)
263 | case "footnote":
264 | guard let content = content else {
265 | return nil
266 | }
267 | let style = AttributedStringStyle(font: .footnote, block: true)
268 | return .font(style: style, child: content)
269 | case "b":
270 | guard let content = content else {
271 | return nil
272 | }
273 | let style = AttributedStringStyle(weight: .bold)
274 | return .font(style: style, child: content)
275 | case "i":
276 | guard let content = content else {
277 | return nil
278 | }
279 | let style = AttributedStringStyle(italic: true)
280 | return .font(style: style, child: content)
281 | case "u":
282 | guard let content = content else {
283 | return nil
284 | }
285 | let style = AttributedStringStyle(underline: true)
286 | return .font(style: style, child: content)
287 | case "br":
288 | return .newline
289 | case "font":
290 | guard let content = content else {
291 | return nil
292 | }
293 | var color: Color? = nil
294 | var font: Font? = nil
295 | if let hexString = attributes["color"] {
296 | //Source: https://www.hackingwithswift.com/example-code/uicolor/how-to-convert-a-html-name-string-into-a-uicolor
297 | var r: Double = 0.0
298 | var g: Double = 0.0
299 | var b: Double = 0.0
300 | var a: Double = 0.0
301 |
302 | if hexString.hasPrefix("#") {
303 | let start = hexString.index(hexString.startIndex, offsetBy: 1)
304 | let hexColor = hexString[start...]
305 |
306 | if hexColor.count == 8 {
307 | let scanner = Scanner(string: String(hexColor))
308 | var hexNumber: UInt64 = 0
309 |
310 | if scanner.scanHexInt64(&hexNumber) {
311 | r = Double((hexNumber & 0xff000000) >> 24) / 255
312 | g = Double((hexNumber & 0x00ff0000) >> 16) / 255
313 | b = Double((hexNumber & 0x0000ff00) >> 8) / 255
314 | a = Double(hexNumber & 0x000000ff) / 255
315 | }
316 | }else if hexColor.count == 6 {
317 | let scanner = Scanner(string: String(hexColor))
318 | var hexNumber: UInt64 = 0
319 |
320 | if scanner.scanHexInt64(&hexNumber) {
321 | r = Double((hexNumber & 0xff0000) >> 16) / 255
322 | g = Double((hexNumber & 0x00ff00) >> 8) / 255
323 | b = Double(hexNumber & 0x0000ff) / 255
324 | a = 1.0
325 | }
326 | }
327 | }
328 | color = Color(.sRGBLinear, red: r, green: g, blue: b, opacity: a)
329 | }
330 | if let family = attributes["family"],
331 | let sizeString = attributes["size"],
332 | let size = NumberFormatter().number(from: sizeString)?.floatValue {
333 | font = Font.custom(family, size: CGFloat(size))
334 | }
335 | let style = AttributedStringStyle(font: font, color: color)
336 | return .font(style: style, child: content)
337 | default:
338 | return content
339 | }
340 | }
341 | func parseUntilWhiteSpace() -> String {
342 | var buffer = ""
343 | while !eof() && !ws.contains(current()){
344 | buffer.append(current())
345 | advance()
346 | }
347 | return buffer
348 | }
349 | func parse() -> Tag {
350 | var array: [Tag] = []
351 | while !eof() {
352 | if current() == "<"{
353 | if eof() || peekIsEof() || peek() == "/" {
354 | break
355 | }
356 | if let tag = parseTag() {
357 | array.append(tag)
358 | }
359 | }else if current() == "&"{
360 | advance()
361 | let entity = parseUntilWhiteSpace()
362 | array.append(.text(entityMap[entity] ?? ""))
363 | } else {
364 | var buffer = ""
365 | while !eof() && !["<", "&"].contains(current()){
366 | buffer.append(current())
367 | advance()
368 | }
369 | array.append(.text(buffer))
370 | }
371 | }
372 | return .array(array)
373 | }
374 | let result = parse()
375 | return result
376 | }
377 | }
378 |
379 | struct AttributedText : View {
380 | var formatted: String
381 | var renderedTag: some View {
382 | let result = Tag.parse(from: formatted).render()
383 | return result
384 | }
385 | var body: some View {
386 | VStack {
387 | renderedTag.lineLimit(nil)
388 | }
389 | }
390 | }
391 |
392 | #if DEBUG
393 | struct AttributedText_Previews : PreviewProvider {
394 | static var previews: some View {
395 | AttributedText(formatted: "test")
396 | }
397 | }
398 | #endif
399 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUIRenderer
4 | //
5 | // Created by Lukas Möller on 19.06.19.
6 | // Copyright © 2019 Lukas Möller. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContentView : View {
12 | @State var string: String = "Type some text here..."
13 | var body: some View {
14 | HStack {
15 | TextField("TEXT", text: $string)
16 | .lineLimit(nil)
17 | AttributedText(formatted: string)
18 | }
19 | .frame(maxWidth: .infinity, maxHeight: .infinity)
20 | }
21 | }
22 |
23 |
24 | #if DEBUG
25 | struct ContentView_Previews : PreviewProvider {
26 | static var previews: some View {
27 | ContentView()
28 | }
29 | }
30 | #endif
31 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2019 Lukas Möller. All rights reserved.
27 | NSMainStoryboardFile
28 | Main
29 | NSPrincipalClass
30 | NSApplication
31 | NSSupportsAutomaticTermination
32 |
33 | NSSupportsSuddenTermination
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/SwiftUIRenderer/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SwiftUIRenderer/SwiftUIRenderer.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------