Luca Cantagallo
Durante l’implementazione di Papyrus, mi sono occupato principalmente della progettazione delle keyword strutturali e testuali del DSL, della generazione e gestione dell’output finale (HTML/PDF), della definizione di alcuni builder, e dell’impostazione dei vincoli semantici tramite tipi raffinati. Di seguito le principali aree sviluppate.
PapyrusPrinter e generazione dei file
Ho progettato e implementato il modulo PapyrusPrinter
, che si occupa di:
- generare e salvare i file HTML e CSS a partire dal risultato del render del contenuto e dello stile,
- creare una directory temporanea se non specificata dall’utente,
- convertire l’HTML in PDF tramite ITextRenderer,
- aprire automaticamente il file generato con il visualizzatore di sistema.
def launchFile(
htmlContent: MainText,
cssContent: StyleText,
title: String,
extension: Extension,
nameFile: String,
outputDirOpt: Option[String]
): Unit =
val targetDir = outputDirOpt
.map(Paths.get(_).resolve(nameFile))
.map(path => if Files.exists(path) then path else Files.createDirectories(path))
.getOrElse(Files.createTempDirectory(s"${title}_tmp"))
val cssFinal = if extension == "pdf" then s"$cssContent\n$cssContentPdf" else cssContent.string
val htmlPath = targetDir.resolve(s"$nameFile.html")
val cssPath = targetDir.resolve(DefaultValues.styleSheet)
Files.writeString(cssPath, cssFinal)
Files.writeString(htmlPath, htmlContent.string)
extension match
case "html" => openInBrowser(htmlPath)
case "pdf" => generatePdf(htmlPath, targetDir.resolve(s"$nameFile.pdf"))
case other => println(s"Unsupported extension: $other")
Keyword e builder
Ho definito diverse keyword DSL assieme ai rispettivi builder:
text
e il relativoTextBuilder
, con supporto a stili inline via extension;title
, che imposta dinamicamente il livello in base al contesto (level(1)
incontent
epapyrus
,level(2)
insection
, ecc.);section
esubsection
, ciascuna con elementi interni, titolo e vincoli semantici;listing
, progettata per supportare strutture annidate e diversi ordinamenti (alphabetical
,length
,reverse
,levenshtein
).
Per la keyword title
, ho implementato una logica di auto-livellamento:
private def generateLevelTitle(builder: TitleBuilder, title: String, ctx: PapyrusBuilder | ContentBuilder | SectionBuilder | SubSectionBuilder): TitleBuilder =
ctx match
case _: SectionBuilder => builder.title(title).level(2)
case _: SubSectionBuilder => builder.title(title).level(3)
case _ => builder.title(title).level(1)
ListBuilder
Il ListBuilder
è una delle componenti più complesse. Supporta:
- inserimento misto di
item
e sottoliste, - ordinamenti multipli tramite pattern matching,
- riordinamento bottom-up preservando l’associazione tra item e sottoliste.
private def sortItemAndSublists(
builder: ListBuilder,
elems: List[ListElementBuilder],
associationMap: Map[ListBuilder, Option[ItemBuilder]]
): List[ListElementBuilder] =
val items = elems.collect { case i: ItemBuilder => i }
val sortedItems = builder.order match
case Some("alphabetical") => items.sortBy(_.value.toLowerCase)
case Some("length") => items.sortBy(_.value.length)
case Some("reverse") => items.reverse
case Some("levenshtein") =>
val ref = builder.reference.getOrElse("")
items.sortBy(i => levenshtein(i.value, ref))
case _ => items
sortedItems.flatMap { item =>
val attachedSublists = associationMap.collect {
case (sublist, Some(`item`)) => sublist
}.toList
item :: attachedSublists
}
private def findSublistParents(builder: ListBuilder): Map[ListBuilder, Option[ItemBuilder]] =
val childMaps = builder.items.collect {
case lb: ListBuilder => findSublistParents(lb)
}.foldLeft(Map.empty[ListBuilder, Option[ItemBuilder]])(_ ++ _)
val localPairs = builder.items.zipWithIndex.collect {
case (sublist: ListBuilder, idx) =>
val maybeItemAbove = if idx > 0 then builder.items(idx - 1) match
case item: ItemBuilder => Some(item)
case _ => None
else None
sublist -> maybeItemAbove
}.toMap
childMaps ++ localPairs
private def visitAllNodesBottomUp(builder: ListBuilder): ListBuilder =
val updatedChildren = builder.items.map {
case lb: ListBuilder => visitAllNodesBottomUp(lb)
case other => other
}
val associationMap = findSublistParents(builder.copyWith(items = updatedChildren))
val updatedItems = sortItemAndSublists(builder, updatedChildren, associationMap)
builder.copyWith(items = updatedItems)

Tipi raffinati e default values
Ho introdotto Iron per validare in compilazione i tipi accettati. Alcuni esempi:
type FontSize = Int :| Interval.Closed[8, 72]
type Extension = String :| Match["html|pdf"]
type SortingList = String :| Match["alphabetical|length|reverse|levenshtein"]
type Alignment = String :| Match["left|right|center|justify"]
Ho inoltre definito i DefaultValues
globali, tra cui:
val fontTitleH1: FontFamily = "Helvetica"
val fontSizeTitleH1: FontSize = 32
val textColorTitleH1: ColorString = "black"
val titleTextH1: String = "Welcome H1"
val extension: Extension = "pdf"
val language: Language = "en"
Costrutti multi-contesto
Infine, ho definito meccanismi per supportare keyword valide in più contesti (es. title
, text
). Questi comportamenti sono definiti con using
impliciti e pattern match.
def title(init: TitleBuilder ?=> TitleBuilder)(using ctx: PapyrusBuilder | ContentBuilder | SectionBuilder | SubSectionBuilder): Unit =
val configuredBuilder = init
val baseTitle = configuredBuilder.build.title
val numberedTitle = generateNumberedTitle(baseTitle, ctx)
val numberedBuilder = generateLevelTitle(configuredBuilder, numberedTitle, ctx)
ctx match
case pb: PapyrusBuilder => pb.setTitle(numberedBuilder.build)
case cb: ContentBuilder => cb.setTitle(numberedBuilder.build)
case sb: SectionBuilder => sb.setTitle(numberedBuilder.build)
case ssb: SubSectionBuilder => ssb.setTitle(numberedBuilder.build)
Immutabilità e proxy per builder complessi
In un primo momento, alcuni builder utilizzavano uno stato mutabile interno per accumulare le configurazioni. In fase di rifinitura ho riscritto l’intero modello in ottica immutabile, dove ogni modifica restituisce una copia aggiornata tramite copy(...)
.
Nel caso di builder semplici come TextBuilder
o TitleBuilder
, è stato sufficiente utilizzare valori val
e metodi with*
, ad esempio:
case class TitleBuilder(
title: String = DefaultValues.titleTextH1,
fontSize: FontSize = DefaultValues.fontSizeTitleH1
) extends Builder[Title]:
def withFontSize(fs: FontSize): TitleBuilder = this.copy(fontSize = fs)
Per builder più complessi e condivisi in contesti DSL impliciti, come MetadataBuilder
o ListBuilder
, è stato necessario introdurre dei proxy, ovvero wrapper che incapsulano lettura/scrittura e mantengono lo stato aggiornato:
class MetadataBuilderProxy(get: () => MetadataBuilder, set: MetadataBuilder => Unit) extends MetadataBuilder:
override def withNameFile(value: String): MetadataBuilder =
val updated = get().withNameFile(value)
set(updated)
this
Questo pattern consente di modificare lo stato in modo immutabile controllato, garantendo la consistenza tra chiamate successive del DSL pur mantenendo la tip-safety e la purezza dei builder. È stato adottato anche nel ListBuilderProxy
, per gestire in sicurezza casi come:
listing:
ordered:
"alphabetical"
item:
"Papyrus"
listing:
item:
"Nested item"
La transizione completa alla struttura immutabile ha migliorato la manutenibilità e l’affidabilità del sistema, evitando effetti collaterali e accoppiamenti nascosti tra costruttori e keyword DSL.