From c3ef95e6bb2b75d9bf80075c01698222e5f46cc1 Mon Sep 17 00:00:00 2001 From: Andrew Valencik Date: Wed, 18 Feb 2026 16:34:35 -0500 Subject: [PATCH] Remove old migration scripts --- migrate-authors.scala | 101 -------------- migrate-events.scala | 302 ------------------------------------------ migrate-posts.scala | 170 ------------------------ 3 files changed, 573 deletions(-) delete mode 100644 migrate-authors.scala delete mode 100644 migrate-events.scala delete mode 100644 migrate-posts.scala diff --git a/migrate-authors.scala b/migrate-authors.scala deleted file mode 100644 index 722fd764..00000000 --- a/migrate-authors.scala +++ /dev/null @@ -1,101 +0,0 @@ -//> using dep org.virtuslab::scala-yaml::0.3.1 -//> using dep co.fs2::fs2-io::3.12.2 - -import cats.effect.{IO, IOApp} -import cats.syntax.all.* -import fs2.io.file.{Files, Path, Flags} -import org.virtuslab.yaml.* - -case class OldAuthor( - full_name: String, - twitter: Option[String], - github: Option[String], - bio: Option[String] -) derives YamlCodec { - def toNew(key: String): NewAuthor = { - // Using GH avatars instead of old `portrait` image for now - val avatar = github.map(gh => s"https://github.com/$gh.png") - NewAuthor( - key = key, - name = full_name, - avatar = avatar, - github = github, - twitter = twitter, - bio = bio - ) - } -} - -case class NewAuthor( - key: String, - name: String, - avatar: Option[String], - github: Option[String], - twitter: Option[String], - bio: Option[String] -) { - def toHocon: String = { - val avatarLine = avatar.map(av => s""" avatar: "$av"""") - val githubLine = github.map(gh => s" github: $gh") - val twitterLine = twitter.map(tw => s" twitter: $tw") - val bioLine = bio.map(b => s""" bio: "$b"""") - - val lines = List( - Some(s"$key {"), - Some(s""" name: "$name""""), - avatarLine, - githubLine, - twitterLine, - bioLine, - Some("}") - ).flatten - - lines.mkString("\n") + "\n" - } -} - -object MigrateAuthors extends IOApp.Simple { - val authorsYamlPath = Path("_data/authors.yml") - val directoryConfPath = Path("src/blog/directory.conf") - val alreadyMigrated = Set( - "armanbilge", - "djspiewak", - "jducoeur", - "valencik", - "samspills", - "lukajcb", - "mpilquist", - "satabin", - "hkateu", - "bpholt", - "rossabaker", - "typelevel", - "foundation" - ) - - def readAuthorsYaml: IO[String] = Files[IO] - .readAll(authorsYamlPath) - .through(fs2.text.utf8.decode) - .compile - .string - - def appendToDirectoryConf(content: String): IO[Unit] = fs2.Stream - .emit(content) - .through(fs2.text.utf8.encode) - .through(Files[IO].writeAll(directoryConfPath, Flags.Append)) - .compile - .drain - - def run: IO[Unit] = for { - yamlContent <- readAuthorsYaml - authorsMap <- IO.fromEither(yamlContent.as[Map[String, OldAuthor]]) - newAuthors = authorsMap.toList - .filterNot { case (key, _) => alreadyMigrated.contains(key) } - .sortBy(_._1) - .map { case (key, oldAuthor) => oldAuthor.toNew(key) } - hoconContent = "\n" + newAuthors.map(_.toHocon).mkString("\n") - - _ <- appendToDirectoryConf(hoconContent) - _ <- IO.println(s"Migrated ${newAuthors.size} authors") - } yield () -} diff --git a/migrate-events.scala b/migrate-events.scala deleted file mode 100644 index 89c537f1..00000000 --- a/migrate-events.scala +++ /dev/null @@ -1,302 +0,0 @@ -//> using scala 3.6.3 -//> using dep org.virtuslab::scala-yaml::0.3.1 -//> using dep co.fs2::fs2-io::3.12.2 -//> using dep com.typesafe:config:1.4.5 - -import cats.effect.{IO, IOApp} -import cats.syntax.all.* -import fs2.io.file.{Files, Path} -import org.virtuslab.yaml.* -import com.typesafe.config.{Config, ConfigFactory} -import java.time.LocalDate -import scala.jdk.CollectionConverters.* - -case class ScheduleItem( - time: String, - title: String, - speakers: Option[List[String]], - summary: Option[String] -) derives YamlCodec - -case class Sponsor( - name: String, - logo: String, - link: String, - `type`: String, - height: Option[Int] -) derives YamlCodec - -case class Meta(meetup: Option[String]) derives YamlCodec - -case class EventConfig( - title: String, - short_title: Option[String], - date_string: String, - location: String, - description: String, - poster_hero: Option[String], - poster_thumb: Option[String], - schedule: Option[List[ScheduleItem]], - sponsors: Option[List[Sponsor]], - meta: Option[Meta] -) derives YamlCodec - -case class Event(conf: EventConfig, content: String, originalYaml: String) { - - def loadSpeakerDirectory(): Map[String, String] = - try { - val config = ConfigFactory.parseFile( - Path("src/blog/directory.conf").toNioPath.toFile - ) - val speakerNames = config - .root() - .keySet() - .asScala - .toList - .map { key => - key -> config.getConfig(key).getString("name") - } - .toMap - speakerNames - } catch { - case _: Exception => Map.empty[String, String] - } - - def cleanOtherLinks(markdown: String): String = { - var cleaned = markdown - - // https://typelevel.org/blog/YYYY/MM/DD/post-name.html -> post-name.md - val typelevelBlogPattern = - """https://typelevel\.org/blog/\d{4}/\d{2}/\d{2}/([^)\s]+)\.html""".r - cleaned = typelevelBlogPattern.replaceAllIn(cleaned, "$1.md") - - // /blog/YYYY/MM/DD/post-name.html -> post-name.md - val relativeBlogPattern = - """(? meetupPattern.replaceAllIn(cleaned, meetupUrl) - case None => meetupPattern.replaceAllIn(cleaned, "") - } - - // Replace .html extensions with .md in relative links (but not absolute URLs starting with http) - val htmlToMdPattern = """(? - val tableRows = scheduleItems - .map { item => - val timeColumn = item.time - - val talkColumn = if (item.speakers.isEmpty) { - item.title - } else { - item.speakers - .map { speakers => - val speakerDirectory = loadSpeakerDirectory() - val speakerNames = speakers - .map(s => speakerDirectory.getOrElse(s, s)) - .mkString(", ") - item.summary match - case Some(value) => - s"@:style(schedule-title)${item.title}@:@ @:style(schedule-byline)${speakerNames}@:@ ${value}" - case None => s"**${item.title}**
${speakerNames}" - } - .getOrElse(item.title) - } - - s"| ${timeColumn} | ${talkColumn} |" - } - .mkString("\n") - - s"""|| Time | Talk | - ||------|------| - |$tableRows""".stripMargin - } - .getOrElse("") - - def generateSponsorsHtml(): String = { - conf.sponsors - .map { sponsors => - val sponsorsByType = sponsors.groupBy(_.`type`) - - val sections = List("platinum", "gold", "silver").flatMap { - sponsorType => - sponsorsByType.get(sponsorType).map { typeSponsors => - val sponsorCells = typeSponsors - .map { sponsor => - s"@:style(bulma-cell bulma-has-text-centered)[@:image(${sponsor.logo}) { alt: ${sponsor.name}, title: ${sponsor.name}, style: legacy-event-sponsor }](${sponsor.link})@:@" - } - .mkString("\n") - - s"""|### ${sponsorType.capitalize} - |@:style(bulma-grid bulma-is-col-min-12) - |$sponsorCells - |@:@""".stripMargin - } - } - - sections.mkString("\n\n") - } - .getOrElse("") - } - - def toLaika(date: String, stage: Int): String = { - val tags = Option - .when(conf.title.contains("Summit"))("summits") - .toList ::: "events" :: Nil - val metadata = - buildHoconMetadata(date, conf.date_string, conf.location, tags) - val title = s"# ${conf.title}" - val image = - conf.poster_hero.map(img => s"![${conf.title}]($img)").getOrElse("") - - stage match { - case 1 => - // Stage 1: Just move to new location, keep original format - s"---\n$originalYaml---\n\n$content\n" - - case 2 => - // Stage 2: HOCON metadata + title, no content changes - s"$metadata\n\n$title\n\n$image\n\n$content\n" - - case 3 => - // Stage 3: Stage 2 + link cleaning - val transformedContent = cleanOtherLinks(content) - s"$metadata\n\n$title\n\n$image\n\n$transformedContent\n" - - case _ => - // Stage 4+: Use original content and replace Jekyll includes with generated HTML - val transformedContent = cleanOtherLinks(content) - - // Replace Jekyll includes with generated HTML - var processedContent = transformedContent - - // Remove schedule assign and replace schedule include - val scheduleAssignPattern = - """\{\%\s*assign\s+schedule\s*=\s*page\.schedule\s*%\}\s*""".r - processedContent = - scheduleAssignPattern.replaceAllIn(processedContent, "") - - val schedulePattern = """\{\%\s*include\s+schedule\.html\s*%\}""".r - val scheduleReplacement = - if (conf.schedule.isDefined) generateScheduleMarkdown() else "" - processedContent = - schedulePattern.replaceAllIn(processedContent, scheduleReplacement) - - // Replace sponsors include - val sponsorsPattern = """\{\%\s*include\s+sponsors\.html\s*%\}""".r - val sponsorsReplacement = - if (conf.sponsors.isDefined) generateSponsorsHtml() else "" - processedContent = - sponsorsPattern.replaceAllIn(processedContent, sponsorsReplacement) - - // Remove venue_map includes (not supported) - val venueMapPattern = """\{\%\s*include\s+venue_map\.html\s*%\}""".r - processedContent = venueMapPattern.replaceAllIn(processedContent, "") - - s"$metadata\n\n$title\n\n$image\n\n$processedContent\n" - } - } -} - -object EventParser { - def parse(path: Path, content: String): Either[Throwable, Event] = { - // Normalize Windows line endings to Unix - val normalized = content.replace("\r\n", "\n") - val parts = normalized.split("---\n", 3) - if (parts.length < 3) { - val fn = path.fileName - Left(new Exception(s"Invalid event '$fn': no YAML front matter found")) - } else { - val yamlContent = parts(1) - val markdownContent = parts(2).trim - yamlContent - .as[EventConfig] - .map(conf => Event(conf, markdownContent, yamlContent)) - } - } -} - -object MigrateEvents extends IOApp { - val oldEventsDir = Path("../typelevel.github.com/collections/_events") - val newBlogDir = Path("src/blog") - - def getDateAndName(path: Path): Either[Throwable, (String, String)] = { - val filename = path.fileName.toString - val datePattern = """(\d{4}-\d{2}-\d{2})-(.+)""".r - filename match { - case datePattern(date, rest) => - Right((date, filename)) // Keep full filename - case _ => - Left(new Exception(s"Filename doesn't match pattern: $filename")) - } - } - - def readEvent(path: Path): IO[String] = Files[IO] - .readAll(path) - .through(fs2.text.utf8.decode) - .compile - .string - - def writeEvent(path: Path, content: String): IO[Unit] = fs2.Stream - .emit(content) - .through(fs2.text.utf8.encode) - .through(Files[IO].writeAll(path)) - .compile - .drain - - def migrateEvent(sourcePath: Path, stage: Int): IO[String] = for { - (date, fullFilename) <- IO.fromEither(getDateAndName(sourcePath)) - content <- readEvent(sourcePath) - event <- IO.fromEither(EventParser.parse(sourcePath, content)) - laikaContent = event.toLaika(date, stage) - destPath = newBlogDir / fullFilename - _ <- writeEvent(destPath, laikaContent) - } yield fullFilename - - def migrateAllEvents(stage: Int): IO[Long] = Files[IO] - .list(oldEventsDir) - .filter(_.fileName.toString.matches("""^\d{4}-\d{2}-\d{2}-.+\.md$""")) - .evalMap(path => migrateEvent(path, stage)) - .evalMap(fullFilename => IO.println(s"Migrated: $fullFilename")) - .compile - .count - - def run(args: List[String]): IO[cats.effect.ExitCode] = { - val stage = args.headOption.flatMap(_.toIntOption).getOrElse(4) - IO.println(s"Running migration with stage $stage") *> - migrateAllEvents(stage) - .flatMap(c => IO.println(s"Migrated $c events")) - .as(cats.effect.ExitCode.Success) - } -} diff --git a/migrate-posts.scala b/migrate-posts.scala deleted file mode 100644 index 63d12697..00000000 --- a/migrate-posts.scala +++ /dev/null @@ -1,170 +0,0 @@ -//> using scala 3.6.3 -//> using dep org.virtuslab::scala-yaml::0.3.1 -//> using dep co.fs2::fs2-io::3.12.2 - -import cats.effect.{IO, IOApp} -import cats.syntax.all.* -import fs2.io.file.{Files, Path} -import org.virtuslab.yaml.* - -case class PostMeta(author: Option[String]) derives YamlCodec - -case class Conf(title: String, category: Option[String], meta: Option[PostMeta]) - derives YamlCodec - -case class Post(conf: Conf, content: String, originalYaml: String) { - - def cleanPostUrl(markdown: String): String = { - // Replace {% post_url YYYY-MM-DD-filename %} with filename.md - val postUrlPattern = """\{%\s*post_url\s+\d{4}-\d{2}-\d{2}-(.+?)\s*%\}""".r - postUrlPattern.replaceAllIn(markdown, "$1.md") - } - - def cleanOtherLinks(markdown: String): String = { - var cleaned = markdown - - // Replace absolute typelevel.org blog URLs: https://typelevel.org/blog/YYYY/MM/DD/post-name.html with post-name.md - val typelevelBlogPattern = - """https://typelevel\.org/blog/\d{4}/\d{2}/\d{2}/([^)\s]+)\.html""".r - cleaned = typelevelBlogPattern.replaceAllIn(cleaned, "$1.md") - - // Replace relative blog URLs: /blog/YYYY/MM/DD/post-name.html with post-name.md - val relativeBlogPattern = - """(? s" author: $${$a}") - val dateLine = Some(s""" date: "$date"""") - val tagsLine = conf.category.map(c => s" tags: [$c]") - - List( - Some("{%"), - authorLine, - dateLine, - tagsLine, - Some("%}") - ).flatten.mkString("\n") - } - - def toLaika(date: String, stage: Int): String = { - val metadata = buildHoconMetadata(date) - val title = s"# ${conf.title}" - - stage match { - case 1 => - // Stage 1: Just move to new location, keep original format - s"---\n$originalYaml---\n\n$content\n" - - case 2 => - // Stage 2: HOCON metadata + title, no content changes - s"$metadata\n\n$title\n\n$content\n" - - case 3 => - // Stage 3: Stage 2 + post_url substitution - val transformedContent = cleanPostUrl(content) - s"$metadata\n\n$title\n\n$transformedContent\n" - - case _ => - // Stage 4+: All transformations - val transformedContent = cleanOtherLinks(cleanPostUrl(content)) - s"$metadata\n\n$title\n\n$transformedContent\n" - } - } -} - -object PostParser { - def parse(path: Path, content: String): Either[Throwable, Post] = { - // Normalize Windows line endings to Unix - val normalized = content.replace("\r\n", "\n") - val parts = normalized.split("---\n", 3) - if (parts.length < 3) { - val fn = path.fileName - Left(new Exception(s"Invalid post '$fn': no YAML front matter found")) - } else { - val yamlContent = parts(1) - val markdownContent = parts(2).trim - yamlContent.as[Conf].map(conf => Post(conf, markdownContent, yamlContent)) - } - } -} - -object MigratePosts extends IOApp { - val oldPostsDir = Path("../typelevel.github.com/collections/_posts") - val newBlogDir = Path("src/blog") - - // Manual renaming map for files that would collide after date stripping - val renameMap: Map[String, String] = Map( - "2023-02-23-gsoc.md" -> "gsoc-2023.md", - "2024-03-02-gsoc.md" -> "gsoc-2024.md", - "2025-02-27-gsoc.md" -> "gsoc-2025.md" - ) - - def getDateAndName(path: Path): Either[Throwable, (String, String)] = { - val filename = path.fileName.toString - val datePattern = """(\d{4}-\d{2}-\d{2})-(.+)""".r - filename match { - case datePattern(date, rest) => - val newName = renameMap.getOrElse(filename, rest) - Right((date, newName)) - case _ => - Left(new Exception(s"Filename doesn't match pattern: $filename")) - } - } - - def readPost(path: Path): IO[String] = Files[IO] - .readAll(path) - .through(fs2.text.utf8.decode) - .compile - .string - - def writePost(path: Path, content: String): IO[Unit] = fs2.Stream - .emit(content) - .through(fs2.text.utf8.encode) - .through(Files[IO].writeAll(path)) - .compile - .drain - - def migratePost(sourcePath: Path, stage: Int): IO[String] = for { - (date, newFilename) <- IO.fromEither(getDateAndName(sourcePath)) - content <- readPost(sourcePath) - post <- IO.fromEither(PostParser.parse(sourcePath, content)) - laikaContent = post.toLaika(date, stage) - destPath = newBlogDir / newFilename - _ <- writePost(destPath, laikaContent) - } yield newFilename - - def migrateAllPosts(stage: Int): IO[Long] = Files[IO] - .list(oldPostsDir) - .filter(_.fileName.toString.matches("""^\d{4}-\d{2}-\d{2}-.+\.md$""")) - .evalMap(path => migratePost(path, stage)) - .evalMap(newFilename => IO.println(s"Migrated: $newFilename")) - .compile - .count - - def run(args: List[String]): IO[cats.effect.ExitCode] = { - val stage = args.headOption.flatMap(_.toIntOption).getOrElse(4) - IO.println(s"Running migration with stage $stage") *> - migrateAllPosts(stage) - .flatMap(c => IO.println(s"Migrated $c posts")) - .as(cats.effect.ExitCode.Success) - } -}