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 { if factor == 0 {
return AttributedString(ingredient) return ingredient
} }
// Match mixed fractions first // Match mixed fractions first
var matches = ObservableRecipeDetail.matchPatternAndMultiply( var matches = ObservableRecipeDetail.matchPatternAndMultiply(
.mixedFraction, .mixedFraction,
in: ingredient, in: String(ingredient.characters),
multFactor: factor multFactor: factor
) )
// Then match fractions, exclude mixed fraction ranges // Then match fractions, exclude mixed fraction ranges
matches.append(contentsOf: matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply( ObservableRecipeDetail.matchPatternAndMultiply(
.fraction, .fraction,
in: ingredient, in: String(ingredient.characters),
multFactor: factor, multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 }) excludedRanges: matches.map({ tuple in tuple.1 })
) )
@@ -116,7 +154,7 @@ class ObservableRecipeDetail: ObservableObject {
matches.append(contentsOf: matches.append(contentsOf:
ObservableRecipeDetail.matchPatternAndMultiply( ObservableRecipeDetail.matchPatternAndMultiply(
.number, .number,
in: ingredient, in: String(ingredient.characters),
multFactor: factor, multFactor: factor,
excludedRanges: matches.map({ tuple in tuple.1 }) excludedRanges: matches.map({ tuple in tuple.1 })
) )
@@ -124,7 +162,8 @@ class ObservableRecipeDetail: ObservableObject {
// Sort matches by match range lower bound, descending. // Sort matches by match range lower bound, descending.
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound}) matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
var attributedString = AttributedString(ingredient) var attributedString = ingredient
for (newSubstring, matchRange) in matches { for (newSubstring, matchRange) in matches {
guard let range = Range(matchRange, in: attributedString) else { continue } guard let range = Range(matchRange, in: attributedString) else { continue }
var attributedSubString = AttributedString(newSubstring) var attributedSubString = AttributedString(newSubstring)

View File

@@ -109,7 +109,7 @@ struct RecipeDetail: Codable {
totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime) totalTime = try container.decodeIfPresent(String.self, forKey: .totalTime)
description = try container.decode(String.self, forKey: .description) description = try container.decode(String.self, forKey: .description)
url = try container.decode(String.self, forKey: .url) 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) recipeCategory = try container.decode(String.self, forKey: .recipeCategory)
tool = try container.decode([String].self, forKey: .tool) tool = try container.decode([String].self, forKey: .tool)
recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient) recipeIngredient = try container.decode([String].self, forKey: .recipeIngredient)

View File

@@ -19,7 +19,7 @@ struct RecipeListSection: View {
ForEach(list, id: \.self) { item in ForEach(list, id: \.self) { item in
HStack(alignment: .top) { HStack(alignment: .top) {
Text("\u{2022}") Text("\u{2022}")
Text("\(item)") Text(ObservableRecipeDetail.applyMarkdownStyling(item))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
} }
.padding(4) .padding(4)

View File

@@ -129,7 +129,7 @@ fileprivate struct IngredientListItem: View {
.foregroundStyle(.red) .foregroundStyle(.red)
} }
if unmodified { if unmodified {
Text(ingredient) Text(ObservableRecipeDetail.applyMarkdownStyling(ingredient))
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(5) .lineLimit(5)
} else { } else {
@@ -142,9 +142,9 @@ fileprivate struct IngredientListItem: View {
} }
.onChange(of: servings) { newServings in .onChange(of: servings) { newServings in
if recipeYield == 0 { if recipeYield == 0 {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings) modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(ingredient), by: newServings)
} else { } else {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield) modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ObservableRecipeDetail.applyMarkdownStyling(ingredient), by: newServings/recipeYield)
} }
} }
.foregroundStyle(isSelected ? Color.secondary : Color.primary) .foregroundStyle(isSelected ? Color.secondary : Color.primary)

View File

@@ -47,7 +47,7 @@ fileprivate struct RecipeInstructionListItem: View {
HStack(alignment: .top) { HStack(alignment: .top) {
Text("\(index)") Text("\(index)")
.monospaced() .monospaced()
Text(instruction) Text(ObservableRecipeDetail.applyMarkdownStyling(instruction))
}.padding(4) }.padding(4)
.foregroundStyle(isSelected ? Color.secondary : Color.primary) .foregroundStyle(isSelected ? Color.secondary : Color.primary)
.onTapGesture { .onTapGesture {