Markdown support in recipe ingredients, instructions and tools

This commit is contained in:
VincentMeilinger
2025-05-26 17:30:36 +02:00
parent b66ef63b6a
commit 6cecdcf1fd
6 changed files with 51 additions and 12 deletions

View File

@@ -93,21 +93,59 @@ class ObservableRecipeDetail: ObservableObject {
)
}
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
static func applyMarkdownStyling(_ text: String) -> AttributedString {
if var markdownString = try? AttributedString(
markdown: text,
options: .init(
allowsExtendedAttributes: true,
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible
)
) {
for (intentBlock, intentRange) in markdownString.runs[AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self].reversed() {
guard let intentBlock = intentBlock else { continue }
for intent in intentBlock.components {
switch intent.kind {
case .header(level: let level):
switch level {
case 1:
markdownString[intentRange].font = .system(.title).bold()
case 2:
markdownString[intentRange].font = .system(.title2).bold()
case 3:
markdownString[intentRange].font = .system(.title3).bold()
default:
break
}
default:
break
}
}
if intentRange.lowerBound != markdownString.startIndex {
markdownString.characters.insert(contentsOf: "\n", at: intentRange.lowerBound)
}
}
return markdownString
}
return AttributedString(text)
}
static func adjustIngredient(_ ingredient: AttributedString, by factor: Double) -> AttributedString {
if factor == 0 {
return AttributedString(ingredient)
return ingredient
}
// Match mixed fractions first
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
.mixedFraction,
in: ingredient,
in: String(ingredient.characters),
multFactor: factor
)
// Then match fractions, exclude mixed fraction ranges
matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply(
.fraction,
in: ingredient,
in: String(ingredient.characters),
multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 })
)
@@ -116,7 +154,7 @@ class ObservableRecipeDetail: ObservableObject {
matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply(
.number,
in: ingredient,
in: String(ingredient.characters),
multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 })
)
@@ -124,7 +162,8 @@ class ObservableRecipeDetail: ObservableObject {
// Sort matches by match range lower bound, descending.
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
var attributedString = AttributedString(ingredient)
var attributedString = ingredient
for (newSubstring, matchRange) in matches {
guard let range = Range(matchRange, in: attributedString) else { continue }
var attributedSubString = AttributedString(newSubstring)

View File

@@ -109,7 +109,7 @@ struct RecipeDetail: Codable {
totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime)
description = try container.decode(String.self, forKey: .description)
url = try container.decode(String.self, forKey: .url)
recipeYield = try container.decode(Int.self, forKey: .recipeYield)
recipeYield = try container.decode(Int?.self, forKey: .recipeYield) ?? 1
recipeCategory = try container.decode(String.self, forKey: .recipeCategory)
tool = try container.decode([String].self, forKey: .tool)
recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient)