diff --git a/admin/site/config.json b/admin/site/config.json --- a/admin/site/config.json +++ b/admin/site/config.json @@ -1,60 +1,59 @@ { "baseURL": "/", "languageCode": "en-gb", "title": "Archive of Formal Proofs", "theme": "afp", "publishDir": "..", "taxonomies": { "author": "authors", "topic": "topics", "dependency": "dependencies" }, "params": { "description": "A collection of proof libraries, examples, and larger scientific developments, mechanically checked in the theorem prover Isabelle.", "images": [ "/images/afp.png" ], "title": "Archive of Formal Proofs", "mainSections": ["entries"], "afpUrls": { "html": "https://www.isa-afp.org/browser_info/current/AFP", "htmlDevel": "https://devel.isa-afp.org/browser_info/current/AFP" - }, - "devel": true + } }, "markup": { "goldmark": { "renderer": { "unsafe": true } } }, "menu": { "main": [ { "name": "Topics", "url": "/topics/", "weight": 2 } ] }, "outputs": { "home": ["html", "rss", "json"], "taxonomyTerm": ["html", "json"] }, "related": { "includeNewer": true, "indices": [ { "name": "topics", "weight": 1 }, { "name": "keywords", "weight": 3 } ], "threshold": 80, "toLower": true } } \ No newline at end of file diff --git a/admin/site/themes/afp/assets/sass/main.scss b/admin/site/themes/afp/assets/sass/main.scss --- a/admin/site/themes/afp/assets/sass/main.scss +++ b/admin/site/themes/afp/assets/sass/main.scss @@ -1,1073 +1,1088 @@ //colors $theme: hsl(242, 27%, 24%); $theme-hover: hsl(241, 26%, 36%); $theme-lightest: hsl(261, 36%, 86%); $theme-focus: #acabcf; $link: #0645ad; $link-hover: #0000ee; $link-visited: #42047c; $white: #fff; $lightest-grey: #f2f2f2; $lighter-grey: #ddd; $light-grey: #bbb; $dark-grey: #777; $darker-grey: #555; $darkest-grey: #303030; $black: #000; $black_15: rgba(0, 0, 0, 0.15); //fonts $sans-serif: "Open Sans", sans-serif; $monospace: "Consolas", monospace; $OpenSans: "Open Sans"; $light: 300; $normal: 400; $bold: 700; $border-bottom: 2px $darker-grey solid; %code { font-family: $monospace; background-color: $lightest-grey; color: $darkest-grey; } %button { border: 1px solid $dark-grey; font-size: 1rem; vertical-align: middle; background-color: $white; color: $black; line-height: 0; font-family: $sans-serif; } %button-image { position: relative; top: 0.12em; left: 5px; padding-right: 5px; } %sticky { position: sticky; top: 0; background-color: $white; margin: 0; padding-bottom: 0.5rem; } @font-face { font-family: $OpenSans; font-display: swap; font-style: normal; font-weight: 300; src: local(""), url("/font/open-sans-v18-latin-300.woff2") format("woff2"), url("/font/open-sans-v18-latin-300.woff") format("woff"); } @font-face { font-family: $OpenSans; font-display: swap; font-style: normal; font-weight: 400; src: local(""), url("/font/open-sans-v18-latin-regular.woff2") format("woff2"), url("/font/open-sans-v18-latin-regular.woff") format("woff"); } @font-face { font-family: $OpenSans; font-display: swap; font-style: italic; font-weight: 400; src: local(""), url("/font/open-sans-v18-latin-italic.woff2") format("woff2"), url("/font/open-sans-v18-latin-italic.woff") format("woff"); } @font-face { font-family: $OpenSans; font-display: swap; font-style: normal; font-weight: 700; src: local(""), url("/font/open-sans-v18-latin-700.woff2") format("woff2"), url("/font/open-sans-v18-latin-700.woff") format("woff"); } @font-face { font-family: $OpenSans; font-display: swap; font-style: italic; font-weight: 700; src: local(""), url("/font/open-sans-v18-latin-700italic.woff2") format("woff2"), url("/font/open-sans-v18-latin-700italic.woff") format("woff"); } *::selection { background-color: $theme; color: $white; } body { color: $black; background: $white; font-family: $sans-serif; display: grid; grid-template-columns: #{"max(20%, 200px)"} 1fr; grid-template-areas: "sidebar content"; justify-items: center; margin: 0; } aside { grid-area: sidebar; background-color: $theme; width: 20vw; margin: 0; height: 100%; position: fixed; top: 0; left: 0; min-width: 200px; word-break: break-all; overflow-y: auto; color: $white; nav { display: flex; flex-direction: column; justify-content: space-between; height: 100vh; } ul { list-style: none; padding: 0; } li { padding: 0.5em 2rem; &:hover { background-color: $theme-hover; } } .active { background-color: $theme-hover; } div > a { display: block; text-align: center; } a { &:link, &:visited { color: $white; } &:focus { outline: 1px $theme-focus solid; outline-offset: 3.5px; } } hr { margin: 2rem; border: none; border-top: 2px solid $dark-grey; } } .logo { width: 40%; height: auto; padding: 10%; max-width: 160px; } li { margin: 0.25em 0; } header { border-bottom: $border-bottom; margin-bottom: 20px; padding: 0.5rem 0; > div { display: flex; justify-content: space-between; padding: 0.5rem 0; } form { padding: 0.5rem 0; float: right; } h1 { padding: 0.5rem 0; margin: 0; clear: both; line-height: 2.75rem; } #searchInput { height: 2rem; padding: 7px; width: 175px; } #searchButton { height: 2rem; padding: 7px; width: 2rem; &:hover { background-color: $lighter-grey; } &:active { background-color: $light-grey; } } div p { margin: 0; } } .content { grid-area: content; margin: 0 auto 2rem; width: 50vw; min-width: 550px; div { line-height: 1.35em; width: 100%; h1 { border-bottom: $border-bottom; } } > div { form { margin: 2rem auto 0; width: 50%; min-width: 290px; padding: 1rem 0; } #searchButton { height: 2.5rem; width: 25%; min-width: 70px; background: $theme; color: $white; border: 1px $theme solid; } #searchInput { height: 2.5rem; width: 75%; } } .popup { background: $white; box-shadow: 0 0 8px 0 $black_15, 0 4px 10px 0 $black_15; padding: 2.5rem; display: block; width: auto; min-width: 300px; max-width: 90vw; max-height: 75vh; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); overflow-y: auto; } .chart { min-width: 400px; width: 100%; min-height: 400px; max-height: 50vh; background: $white; margin: 20px auto 40px; border: 1px solid $black; position: relative; } .statsnumber { text-align: right; padding-right: 1rem; } ul { list-style-type: none; } li { word-break: break-all; } li::before { content: "–"; position: absolute; margin-left: -1.3em; } } .entries { > div { display: grid; grid-template-columns: 3fr 1fr; } main { padding-right: 2%; max-width: 90ch; div { margin: 1rem 0; } } nav { width: 130px; margin-left: auto; h4 { color: $darker-grey; font-weight: $normal; } } } .theories { grid-template-columns: 20% 80%; justify-content: normal; .collapsible.collapsed { display: none; } .invertible.collapsed { transform: scale(1,-1); } .collapse-container.collapsed { .collapsible { display: none; } .invertible { transform: scale(1,-1); } } .content { min-width: clamp(80ch,50vw, 50vw); width: auto; max-width: 75vw; margin: 0 0 2rem 4vw; } h2 { @extend %sticky; padding-top: 0.5rem; border-bottom: 1px $light-grey solid; svg { height: 2rem; width: 20px; position: absolute; right: 1em; stroke: $dark-grey; transition: transform 100ms; } } main { > div { margin-bottom: 1rem; } } aside { word-break: normal; z-index: 1; overflow-x: hidden; .logo { padding-top: 2rem; padding-bottom: 2rem; width: 100px; @media (min-width: 1800px) { width: 150px; } } > div { margin-right: 1rem; } &:hover { min-width: fit-content; div { min-width: fit-content; } } ul { margin-left: 1.75rem; } li { padding: 0; &:hover { background-color: $theme; } > a:hover { background-color: $theme-hover; } a { padding: 0.5em 0; display: inline-block; width: 100%; } } } @media (max-width: 875px) { padding: 0; grid-template-columns: 1fr; .content { min-width: 350px; max-width: 100vw; margin: 80px 0; } nav { display: none; } } } .links { button { display: inline-block; margin: 0.5rem 0; } a { display: inline-block; margin: 0.5rem 0; } } .popUpButton { &:visited { color: $white; } &:link { border: none; padding: 0.75rem 0; background-color: $theme; color: $white; cursor: pointer; text-align: center; transition: background-color 200ms ease, transform 150ms ease; font-weight: $bold; font-size: 0.9rem; width: 130px; } &:active { transform: scale(0.98); } } a { &:link { color: $link; text-decoration: none; font-weight: $bold; } &:visited { color: $link-visited; } &:hover { color: $link-hover; } } h1 { margin: 1.5em 0 0 0; font-size: 2em; padding-bottom: 20px; span.first { font-size: 2.3rem; } } h2 { margin: 1.5em 0 0 0; padding: 0; line-height: normal; } h3 { margin: 1.5em 0 0 0; font-stretch: normal; font-size: larger; padding: 0; font-weight: $light; &:first-child { margin: 0.5rem 0 0 0; } } h4 { margin: 1.5em 0 0 0; margin-bottom: 0.5ex; font-weight: $bold; } .popup { h2 { margin-top: 0; } .close { position: absolute; width: 20px; height: 20px; top: 2rem; right: 2rem; transition: all 200ms; font-size: 24px; font-weight: $bold; color: $darker-grey; text-align: center; &:hover { color: $darkest-grey; } } } #downloadPopUp { a[download] { display: block; width: 154px; margin: 2.5rem auto 0; } } .year { @extend %sticky; padding-top: 2rem; border-bottom: $border-bottom; } img { max-width: 90%; } pre { @extend %code; padding: 1rem; overflow: auto; &.bibtex { white-space: pre; border-width: thin; border-style: solid; } &.code { white-space: pre; padding: 10px 2px; } } code { padding: 1px 2px 2px; @extend %code; } blockquote { border: 2px solid $lighter-grey; padding: 1rem 2rem; margin: auto 0; } tr:nth-child(even) { background-color: $lightest-grey; } .nobr { white-space: nowrap; } .largeTopMargin { margin-top: 48px; } .searchPage { > div { display: grid; grid-template-columns: 3fr #{"max(25%, 175px)"}; grid-template-rows: 150px 1fr; grid-template-areas: "search search" "entries sidebar"; } form { grid-area: search; } #search-main { grid-area: entries; padding-right: 2rem; box-sizing: border-box; } #search-results { > div { position: relative; margin: 1.5rem 0; border-bottom: 2px $lighter-grey solid; padding-bottom: 1.5rem; } div + div { margin-top: 1rem; } .title, .subtitle { display: flex; justify-content: space-between; align-items: center; } .subtitle { color: $dark-grey; font-size: 0.875em; > p { margin: 0; } } a[download] { padding: 5px 7.5px; flex-shrink: 0; margin-left: 1em; } } #search-sidebar { grid-area: sidebar; } } a[download] { display: inline-block; color: $black; height: 22px; padding: 4px 7px; @extend %button; &:focus { outline: none; box-shadow: 0 0 0 2px #c5c4ddbb; } &:hover, &:active { background-color: $light-grey; color: $black; } &:after { content: url("/images/download.svg"); @extend %button-image; } } .entry { display: flex; align-items: center; margin-top: 1.35rem; a[download] { position: absolute; top: 0; right: 0; } .date { text-align: right; width: 15%; min-width: 6ch; font-family: $monospace; } h5 { font-size: initial; display: initial; } } .form-container { width: 290px; text-align: left; } #searchInput { @extend %button; border-right: none; font-family: $sans-serif; padding: 0.5em; } #searchButton { @extend %button; cursor: pointer; &:focus { outline: none; box-shadow: 0 0 0 2px #c5c4ddbb; } img { max-width: 1rem; } } button { @extend %button; height: 2rem; padding: 7px; font-weight: $bold; cursor: pointer; &:hover { background-color: $lighter-grey; } &:active { background-color: $light-grey; } &:focus { outline: none; box-shadow: 0 0 0 2px #c5c4ddbb; } } button[copy] { &:after { content: url("/images/copy.svg"); @extend %button-image; } } #searchInputautocomplete-list { width: 214px; position: absolute; margin-bottom: 24px; padding: 8px 0; background-color: $white; border: 1px solid $lighter-grey; box-shadow: 0 2px 4px $black_15; z-index: 1; div { width: auto; padding: 8px 12px; line-height: 16px; cursor: pointer; &:hover { background: $lighter-grey; } } } .autocomplete-active { background: $lighter-grey; } .item-text { width: 85%; } .date { color: $darker-grey; font-size: 14px; } .overlay { position: fixed; top: 0; bottom: 0; left: 0; right: 0; background: $black_15; transition: opacity 200ms; visibility: hidden; opacity: 0; .cancel { position: absolute; width: 100%; height: 100%; cursor: pointer; } &:target { visibility: visible; opacity: 1; } } +.status-ok { + background-color: #00FF99; + padding: 8px; +} + +.status-skipped { + background-color: #FFFF99; + padding: 8px; +} + +.status-failed { + background-color: #DD2222; + padding: 8px; +} + iframe { width: 100%; height: 90vh; } .loader { margin: 24px 24px 0 0; height: 6px; position: relative; display: block; background-color: $theme; border-radius: 4px; overflow: hidden; .animation { background-color: $light-grey; &:before { content: ''; position: absolute; background-color: inherit; top: 0; left: 0; bottom: 0; will-change: left, right; animation: loader 2s linear infinite; } } } @keyframes loader { 0% { left: -20%; right: 100%; } 100% { left: 100%; right: -20%; } } #menuToggle { input, label { display: none; } > .logoLink, img[alt="Search"] { display: none; } > #menu { box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); } form { display: none; } } @media (max-width: 650px) { .content { width: 90%; min-width: 350px; } .searchPage { > div { display: grid; grid-template-columns: 1fr; grid-template-rows: 150px auto auto ; grid-template-areas: 'search' 'sidebar' 'entries'; } } } @media (max-width: 875px) { body { grid-template-columns: 1fr; grid-template-areas: 'content'; } aside { height: 4rem; width: 100vw; overflow-y: visible; z-index: 1; } header { form { display: none; } } .content { margin-top: 80px; } .entries { > div { display: grid; grid-template-rows: auto auto; grid-template-columns: 1fr; } nav { margin-left: 0; } } .largeTopMargin { margin-top: 0; } #banner { left: 0; width: 100vw; height: 75px; p { line-height: normal; margin: 1rem; } } #menuToggle { background: $theme; display: block; position: relative; top: 0; left: 0; z-index: 1; height: 80px; user-select: none; width: 100vw; a { text-decoration: none; transition: color 0.3s ease; } .logoLink { height: 3rem; max-width: 50vw; top: 1rem; left: 0; right: 0; margin-left: auto; margin-right: auto; img { padding: 0; height: 3rem; object-fit: contain; } } .logoLink, input, label, a[href="/search"] { display: block; position: absolute; z-index: 2; } input, label, a[href="/search"] { width: 24px; height: 24px; top: 28px; } input,label { left: 28px; cursor: pointer; } a[href="/search"] { right: 28px; filter: invert(1); } a[href="/search"] img, label img { width: 100%; } input { opacity: 0; &:checked { ~ nav { left: 0; } } } label span { display: none; } } #menu { height: 100vh; position: absolute; top: 0; left: 0; padding-top: 80px; box-sizing: border-box; min-width: 200px; width: 50vw; max-width: 350px; min-height: 355px; background: $theme; left: -100%; transition: left 0.3s ease; .logoLink { display: none; } } } diff --git a/admin/site/themes/afp/layouts/entries/single.html b/admin/site/themes/afp/layouts/entries/single.html --- a/admin/site/themes/afp/layouts/entries/single.html +++ b/admin/site/themes/afp/layouts/entries/single.html @@ -1,134 +1,154 @@ {{- define "main" -}} {{- $site := . -}} {{- $path := .Site.Params.afpUrls.html -}} -{{- if eq .Site.Params.devel true -}} +{{- if isset .Site.Data "status" -}} {{- $path = .Site.Params.afpUrls.htmlDevel -}} {{- end -}} {{- $entry_name := .File.BaseFileName -}}
+ {{- if isset .Site.Data "status" -}} + + This is a development version of this entry. It might change over time and is not stable. Please refer to release versions for citations. + + {{- end -}}

Abstract

{{- trim .Params.abstract "\n" | safeHTML -}}
{{- if (eq .Params.licence "BSD") -}} BSD License {{- else if (eq .Params.license "LGPL") -}} GNU Lesser General Public License (LGPL) {{- else -}} {{- printf "%s License" .Params.license -}} {{- end -}} {{- with .Params.extra -}} {{- range $key, $value := . -}}

{{- humanize $key -}}

{{- $value | safeHTML -}}

{{- end -}} {{- end -}} {{- $Scratch := newScratch -}} {{- with .Params.dependencies -}}

Depends On

{{- end -}} {{- with (index .Site.Taxonomies.dependencies (urlize $entry_name)) -}}

Used by

{{- end -}} {{- with .Params.topics -}}

Topics

{{- end -}}

Theories

{{ $related := .Site.RegularPages.Related . | first 3 }} {{ with $related }}

Related Entries

{{ end }}
- +{{- if isset .Site.Data "status" -}} + +{{- else -}}
+{{- end -}} {{- end -}} diff --git a/admin/site/themes/afp/layouts/index.html b/admin/site/themes/afp/layouts/index.html --- a/admin/site/themes/afp/layouts/index.html +++ b/admin/site/themes/afp/layouts/index.html @@ -1,38 +1,48 @@ {{- define "main" -}}

The Archive of Formal Proofs is a collection of proof libraries, examples, and larger scientific developments, mechanically checked in the theorem prover Isabelle. It is organized in the way of a scientific journal, is indexed by dblp and has an ISSN: 2150-914x. Submissions are refereed and we encourage companion AFP submissions to conference and journal publications. To cite an entry, please use the preferred citation style.

+{{- if isset .Site.Data "status" -}} +

+ +This is the development version of the archive, referring to a recent Isabelle development version. +Some entries may not be in a working state. +The main archive is available from the front page. + +

+{{- end -}} +
{{- range (where site.RegularPages "Type" "in" site.Params.mainSections).GroupByDate "2006" -}}

{{- .Key -}}

{{- range .Pages }}
{{- htmlUnescape .Title -}}

by {{ partial "authors.html" (dict "site" . "authors" .Params.authors) -}}
{{ .Date.Format "Jan 02" }}
{{- end -}}
{{- end -}} {{- end -}} \ No newline at end of file diff --git a/admin/site/themes/afp/layouts/partials/theory-navigation.html b/admin/site/themes/afp/layouts/partials/theory-navigation.html --- a/admin/site/themes/afp/layouts/partials/theory-navigation.html +++ b/admin/site/themes/afp/layouts/partials/theory-navigation.html @@ -1,32 +1,32 @@ {{- $entry_name := .File.BaseFileName -}} {{- $site := . -}} {{- $path := .Site.Params.afpUrls.html -}} -{{- if eq .Site.Params.devel true -}} +{{- if isset .Site.Data "status" -}} {{- $path = .Site.Params.afpUrls.htmlDevel -}} {{- end -}} diff --git a/admin/site/themes/afp/layouts/theories/single.html b/admin/site/themes/afp/layouts/theories/single.html --- a/admin/site/themes/afp/layouts/theories/single.html +++ b/admin/site/themes/afp/layouts/theories/single.html @@ -1,13 +1,13 @@ {{- define "main" -}} {{- $site := . -}} {{- $path := .Site.Params.afpUrls.html -}} -{{- if eq .Site.Params.devel true -}} +{{- if isset .Site.Data "status" -}} {{- $path = .Site.Params.afpUrls.htmlDevel -}} {{- end -}}
{{- range .Params.theories }}

{{ . }}

{{- end -}}
{{- end -}} \ No newline at end of file diff --git a/tools/afp_site_gen.scala b/tools/afp_site_gen.scala --- a/tools/afp_site_gen.scala +++ b/tools/afp_site_gen.scala @@ -1,265 +1,281 @@ package afp import isabelle._ object AFP_Site_Gen { /* json */ object JSON { type T = isabelle.JSON.T object Object { type T = isabelle.JSON.Object.T } def from_authors(authors: List[Metadata.Author]): T = Metadata.TOML.from_authors(authors) def from_topics(topics: List[Metadata.Topic]): T = Metadata.TOML.from_topics(topics) def from_affiliations(affiliations: List[Metadata.Affiliation]): Object.T = { Utils.group_sorted(affiliations, (a: Metadata.Affiliation) => a.author).view.mapValues(author_affiliations => { val homepage = author_affiliations.collectFirst { case Metadata.Homepage(_, _, url) => url } val email = author_affiliations.collectFirst { case Metadata.Email(_, _, address) => address } isabelle.JSON.Object( homepage.map(s => List("homepage" -> s)).getOrElse(Nil) ::: email.map(s => List("email" -> s)).getOrElse(Nil): _*) }).toMap } def from_change_history(history: Metadata.Change_History): Object.T = { if (history.isEmpty) { Map.empty } else { Map("Change history" -> history.map { case (date, str) => "[" + date.toString + "] " + str }.mkString("\n")) } } def from_entry(entry: Metadata.Entry): JSON.Object.T = isabelle.JSON.Object( "title" -> entry.title :: "authors" -> entry.authors.map(_.author).distinct :: "affiliations" -> from_affiliations(entry.authors ++ entry.contributors) :: (if (entry.contributors.nonEmpty) "contributors" -> entry.contributors.map(_.author).distinct :: Nil else Nil) ::: "date" -> entry.date.toString :: "topics" -> entry.topics.map(_.id) :: "abstract" -> entry.`abstract` :: "license" -> entry.license :: (if (entry.releases.nonEmpty) "releases" -> entry.releases.map(r => r.isabelle -> r.date.toString).toMap :: Nil else Nil) ::: (if (entry.note.nonEmpty) "note" -> entry.note :: Nil else Nil) ::: (if (entry.change_history.nonEmpty || entry.extra.nonEmpty) "extra" -> (from_change_history(entry.change_history) ++ entry.extra) :: Nil else Nil): _*) def from_keywords(keywords: List[String]): T = keywords.zipWithIndex.map { case (keyword, i) => isabelle.JSON.Object("id" -> i, "keyword" -> keyword) } } /* keyword extraction */ private val replacements = List( "<[^>]*>".r -> "", "[^\\w\\s()'.,;:-]".r -> " ", "\\s+".r -> " ") def extract_keywords(text: String): List[String] = { val stripped_text = replacements.foldLeft(text) { case (res, (regex, replacement)) => regex.replaceAllIn(res, replacement) } val arg = quote(stripped_text.replaceAll("\"", "\\\"")) val keyword_cmd = "from keywords import *; print_keywords(" + arg + ")" Python.run(keyword_cmd).check.out_lines } /* site generation */ def afp_site_gen( out_dir: Option[Path], layout: Hugo.Layout, + status_file: Option[Path], afp_structure: AFP_Structure, progress: Progress = new Progress()): Unit = { /* add authors */ progress.echo("Preparing authors...") val authors = afp_structure.load_authors val authors_by_id = Utils.grouped_sorted(authors, (a: Metadata.Author) => a.id) layout.write_data(Path.basic("authors.json"), JSON.from_authors(authors)) /* add topics */ progress.echo("Preparing topics...") val topics = afp_structure.load_topics def sub_topics(topic: Metadata.Topic): List[Metadata.Topic] = topic :: topic.sub_topics.flatMap(sub_topics) val topics_by_id = Utils.grouped_sorted(topics.flatMap(sub_topics), (t: Metadata.Topic) => t.id) layout.write_data(Path.basic("topics.json"), JSON.from_topics(topics)) /* add releases */ progress.echo("Preparing releases...") val releases_by_entry = afp_structure.load_releases.groupBy(_.entry) /* extract keywords */ progress.echo("Extracting keywords...") var seen_keywords = Set.empty[String] val entry_keywords = afp_structure.entries.map(name => { val entry = afp_structure.load_entry(name, authors_by_id, topics_by_id, releases_by_entry) val Keyword = """\('([^']*)', ([^)]*)\)""".r val scored_keywords = extract_keywords(entry.`abstract`).map { case Keyword(keyword, score) => keyword -> score.toDouble case s => error("Could not parse: " + s) } seen_keywords ++= scored_keywords.map(_._1) name -> scored_keywords.filter(_._2 > 1.0).map(_._1) }).toMap seen_keywords = seen_keywords.filter(k => !k.endsWith("s") || !seen_keywords.contains(k.stripSuffix("s"))) layout.write_static(Path.make(List("data", "keywords.json")), JSON.from_keywords(seen_keywords.toList)) def get_keywords(name: Metadata.Entry.Name): List[String] = entry_keywords(name).filter(seen_keywords.contains).take(8) /* add entries and theory listings */ progress.echo("Preparing entries...") val sessions_structure = afp_structure.sessions_structure val sessions_deps = Sessions.deps(sessions_structure) for (name <- afp_structure.entries) { val entry = afp_structure.load_entry(name, authors_by_id, topics_by_id, releases_by_entry) val deps = for { session <- afp_structure.entry_sessions(name) dep <- sessions_structure.imports_graph.imm_preds(session.name) if session.name != dep && sessions_structure(dep).groups.contains("AFP") } yield dep val topo_theories = for { session <- afp_structure.entry_sessions(name) base = sessions_deps(session.name) node <- base.session_theories } yield node.theory_base_name val entry_json = JSON.from_entry(entry) ++ isabelle.JSON.Object( "dependencies" -> deps.distinct, "theories" -> topo_theories, "url" -> ("/entries/" + name + ".html"), "keywords" -> get_keywords(name)) val theories_json = isabelle.JSON.Object( "title" -> entry.title, "url" -> ("/entries/" + name.toLowerCase + "/theories"), "theories" -> topo_theories) layout.write_content(Path.make(List("entries", name + ".md")), entry_json) layout.write_content(Path.make(List("theories", name + ".md")), theories_json) } /* add statistics */ progress.echo("Preparing statistics...") val statistics_cmd = "from statistics import *; add_statistics(" + commas_quote( List( Path.explode("$AFP_BASE").absolute.implode, Path.explode("$AFP").absolute.implode, layout.data_dir.implode)) + ")" Python.run(statistics_cmd).check (layout.data_dir + Path.basic("statistics.html")).file.delete() /* project */ progress.echo("Preparing project files") layout.copy_project() + /* status */ + + status_file match { + case Some(status_file) => + progress.echo("Preparing devel version...") + val status_json = isabelle.JSON.parse(File.read(status_file)) + layout.write_data(Path.basic("status.json"), status_json) + } + + /* hugo */ out_dir match { case Some(out_dir) => progress.echo("Building site...") Hugo.build(layout, out_dir).check progress.echo("Finished building site") case None => progress.echo("Finished sitegen preparation.") } } val isabelle_tool = Isabelle_Tool("afp_site_gen", "generates afp website source", Scala_Project.here, args => { var base_dir = Path.explode("$AFP_BASE") var hugo_dir = base_dir + Path.make(List("web", "hugo")) + var status_file: Option[Path] = None var out_dir: Option[Path] = None val getopts = Getopts(""" Usage: isabelle afp_site_gen [OPTIONS] Options are: -B DIR afp base dir (default """" + base_dir.implode + """") -H DIR generated hugo project dir (default """" + hugo_dir.implode + """") - -O DIR output dir (default none) + -D FILE build status file for devel version + -O DIR output dir for optional build (default none) Generates the AFP website source. HTML files of entries are dynamically loaded. + Providing a status file will build the development version of the archive. Site will be built from generated source if output dir is specified. """, "B:" -> (arg => base_dir = Path.explode(arg)), "H:" -> (arg => hugo_dir = Path.explode(arg)), + "D:" -> (arg => status_file = Some(Path.explode(arg))), "O:" -> (arg => out_dir = Some(Path.explode(arg)))) getopts(args) val afp_structure = AFP_Structure(base_dir) val layout = Hugo.Layout(hugo_dir) val progress = new Console_Progress() progress.echo("Preparing site generation in " + hugo_dir.implode) - afp_site_gen(out_dir = out_dir, layout = layout, afp_structure = afp_structure, progress = progress) + afp_site_gen(out_dir = out_dir, layout = layout, status_file = status_file, + afp_structure = afp_structure, progress = progress) }) } \ No newline at end of file diff --git a/tools/hugo.scala b/tools/hugo.scala --- a/tools/hugo.scala +++ b/tools/hugo.scala @@ -1,79 +1,79 @@ package afp import isabelle._ object Hugo { val hugo_home = Isabelle_System.getenv("ISABELLE_HUGO") val hugo_static = Path.explode("$AFP_BASE") + Path.make(List("admin", "site")) class Layout private(private[Hugo] val src_dir: Path) { private def write(file: Path, content: String): Unit = { val path = src_dir + file if (!path.dir.file.exists()) path.dir.file.mkdirs() File.write(path, content) } val data_dir = src_dir + Path.basic("data") def write_data(file: Path, content: JSON.T): Unit = write(Path.basic("data") + file, isabelle.JSON.Format(content)) val content_dir = src_dir + Path.basic("content") def write_content(file: Path, content: JSON.T): Unit = write(Path.basic("content") + file, isabelle.JSON.Format(content)) val static_dir = src_dir + Path.basic("static") def write_static(file: Path, content: JSON.T): Unit = write(Path.basic("static") + file, isabelle.JSON.Format(content)) def copy_project(): Unit = { if (hugo_static.canonical != src_dir) { for { file <- List( List("content", "_index.md"), List("content", "about.md"), - List("content", "contribution.md"), + List("content", "submission.md"), List("content", "download.md"), List("content", "help.md"), List("content", "search.md"), List("content", "statistics.md"), List("content", "submission.md"), List("themes"), List("config.json")) } Isabelle_System.copy_dir(hugo_static + Path.make(file), src_dir + Path.make(file)) } } } object Layout { def apply(src_dir: Path = Path.explode("$AFP_BASE") + Path.make(List("web", "hugo"))): Layout = new Layout(src_dir.canonical) } private lazy val exec = Path.explode(proper_string(hugo_home).getOrElse(error("No hugo component found"))) + Path.basic("hugo") def build(layout: Layout, out_dir: Path = Path.explode("$AFP_BASE") + Path.basic("web")): Process_Result = { Isabelle_System.bash( exec.implode + " -s " + quote(layout.src_dir.implode) + " -d " + quote(out_dir.canonical.implode)) } def watch(layout: Layout, out_dir: Path = Path.explode("$AFP_BASE") + Path.basic("web"), progress: Progress = new Progress()): Process_Result = { Isabelle_System.bash( exec.implode + " server -s " + quote(layout.src_dir.implode) + " -d " + quote(out_dir.canonical.implode), progress_stdout = progress.echo, progress_stderr = progress.echo_warning) } } \ No newline at end of file