Skip to content

Commit

Permalink
Display bibtex code in DOI citations
Browse files Browse the repository at this point in the history
  • Loading branch information
Ostrzyciel committed Sep 26, 2024
1 parent e2f37f0 commit 1fcb0c7
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 30 deletions.
20 changes: 20 additions & 0 deletions src/main/scala/util/doc/DocAnnotationContext.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.riverbench.ci_worker
package util.doc

trait DocAnnotationContext:
private val annotations = collection.mutable.ArrayBuffer.empty[String]

def registerAnnotation(text: String): Int =
annotations.addOne(text)
annotations.size

protected def wrapSection(text: StringBuilder): StringBuilder =
if annotations.isEmpty then
text
else
text
.insert(0, "<div class=\"annotate\" markdown>\n\n")
.append("\n</div>\n\n")
for i <- annotations.indices do
text.append(f"${i + 1}. ${annotations(i)}\n")
text
7 changes: 4 additions & 3 deletions src/main/scala/util/doc/DocBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class DocBuilder(ontologies: Model, opt: DocBuilder.Options):
rootSection

def buildSection(resource: Resource, section: DocSection): Unit =
given DocAnnotationContext = section
val props = getDocPropsForRes(resource)

val usedValues = props.flatMap { (prop, nodes) =>
Expand Down Expand Up @@ -83,12 +84,12 @@ class DocBuilder(ontologies: Model, opt: DocBuilder.Options):
Some(docProp, statements.map(_.getObject))
}

private def getDocValuesForRes(resource: Resource): Iterable[(DocProp, DocValue)] =
private def getDocValuesForRes(resource: Resource)(using DocAnnotationContext): Iterable[(DocProp, DocValue)] =
getDocPropsForRes(resource).map { (prop, objects) =>
prop -> makeValue(prop, objects)
}

private def makeTable(objects: Iterable[RDFNode]): DocValue =
private def makeTable(objects: Iterable[RDFNode])(using DocAnnotationContext): DocValue =
val tableMap = objects.flatMap {
case rowRes: Resource =>
val rowValues = getDocValuesForRes(rowRes)
Expand All @@ -104,7 +105,7 @@ class DocBuilder(ontologies: Model, opt: DocBuilder.Options):
}.flatten.toMap
DocValue.Table(tableMap)

private def makeValue(docProp: DocProp, objects: Iterable[RDFNode]): DocValue =
private def makeValue(docProp: DocProp, objects: Iterable[RDFNode])(using DocAnnotationContext): DocValue =
def makeValueInternal(o: RDFNode): DocValue =
(docProp.prop, o) match
case opt.customValueFormatters(value) => value
Expand Down
13 changes: 9 additions & 4 deletions src/main/scala/util/doc/DocSection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import util.RdfUtil

import scala.collection.mutable

class DocSection(val level: Int, defaultPropGroup: Option[String] = None, isRoot: Boolean = false):
class DocSection(val level: Int, defaultPropGroup: Option[String] = None, isRoot: Boolean = false)
extends DocAnnotationContext:

private val entries = mutable.ListBuffer[(DocProp, DocValue)]()
private val subsections = mutable.ListBuffer[DocSection]()

Expand Down Expand Up @@ -60,12 +62,13 @@ class DocSection(val level: Int, defaultPropGroup: Option[String] = None, isRoot
selfWeight + subWeight

def toMarkdown: String =
val sb = new StringBuilder()
val headerSb = new StringBuilder()
val anchorMd = anchor.map(a => s" <a name=\"$a\"></a>").getOrElse("")
sb.append(s"${"#"*level}$anchorMd $title\n\n")
headerSb.append(s"${"#"*level}$anchorMd $title\n\n")
if content.nonEmpty then
sb.append(s"$content\n\n")
headerSb.append(s"$content\n\n")

val sb = new StringBuilder()
for entry <- entries.sortBy(_._1.weight) do
if entry._2.noIndent then
sb.append(s"\n${entry._2.toMarkdown}\n")
Expand All @@ -82,4 +85,6 @@ class DocSection(val level: Int, defaultPropGroup: Option[String] = None, isRoot
for sub <- sortedSections do
sb.append(s"${sub.toMarkdown.strip}\n\n")

wrapSection(sb).insert(0, headerSb)

sb.toString
11 changes: 8 additions & 3 deletions src/main/scala/util/doc/DocValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object DocValue:
override def getSortKey: String = label

object Link:
def apply(value: model.Resource): Link =
def apply(value: model.Resource)(using DocAnnotationContext): Link =
val uri = value.getURI
val split = if uri.startsWith(AppConfig.CiWorker.baseDatasetUrl) then
if uri.contains("#statistics-") then
Expand Down Expand Up @@ -104,9 +104,14 @@ object DocValue:
def toMarkdown: String = f"[$name]($uri)"
def getSortKey = name

case class DoiLink(value: model.Resource) extends Link:
case class DoiLink(value: model.Resource)(using DocAnnotationContext) extends Link:
// We assume here that someone earlier preloaded the needed bibliography
private val bib = DoiBibliography.getBibliographyFromCache(value.getURI)
private val bib = DoiBibliography.getBibliographyFromCache(value.getURI).map { entry =>
val annotationId = summon[DocAnnotationContext].registerAnnotation(
f"BibTeX citation:\n ```bibtex\n${entry.bibtex.indent(4).stripLineEnd}\n ```"
)
f"${entry.apa} :custom-bibtex:{ .rb-bibtex } ($annotationId)"
}
def toMarkdown: String = bib.fold(f"[${value.getURI}](${value.getURI})")
(b => urlRegex.replaceAllIn(b, m => f"[${m.matched}](${m.matched})"))
override def getSortKey = value.getURI
Expand Down
41 changes: 21 additions & 20 deletions src/main/scala/util/external/DoiBibliography.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import _root_.io.circe.parser.decode
import scala.util.{Failure, Success, Try}

object DoiBibliography:
case class BibliographyEntry(doi: String, bib: String)
case class BibliographyEntry(doi: String, apa: String, bibtex: String)
case class BibliographyCache(entries: List[BibliographyEntry])

private val workingCache = collection.concurrent.TrieMap.empty[String, String]
private val workingCache = collection.concurrent.TrieMap.empty[String, BibliographyEntry]
private val usedDois = collection.concurrent.TrieMap.empty[String, String]
private val mediaBibliography = MediaRange(MediaType.text("x-bibliography", "style=apa"))
private val mediaBibliographyApa = MediaType.text("x-bibliography", "style=apa")
private val mediaBibtex = MediaType.applicationWithOpenCharset("x-bibtex")

// Initialization code
{
Expand All @@ -33,7 +34,7 @@ object DoiBibliography:
println(s"Failed to parse bibliography cache: $err")
BibliographyCache(Nil)
}, identity)
workingCache ++= cache.entries.map(e => e.doi -> e.bib)
workingCache ++= cache.entries.map(e => e.doi -> e)
println(s"Loaded bibliography cache with ${workingCache.size} entries from $cacheFile")
}

Expand All @@ -43,30 +44,30 @@ object DoiBibliography:
saveCache()
})

def getBibliography(doiLike: String)(using as: ActorSystem[_]): Future[String] =
def getBibliography(doiLike: String)(using as: ActorSystem[_]): Future[BibliographyEntry] =
given ExecutionContext = as.executionContext
Future.fromTry(extractDoi(doiLike)) flatMap { doi =>
usedDois.put(doi, doi)
workingCache.get(doi) match
case Some(bib) => Future.successful(bib)
case Some(entry) => Future.successful(entry)
case None =>
val bib = fetchBibliography(doi)
bib.foreach(b => {
for
apa <- fetchBibliography(doi, mediaBibliographyApa)
bibtex <- fetchBibliography(doi, mediaBibtex)
yield
println(s"Fetched bibliography for $doi")
workingCache.put(doi, b)
})
bib
val entry = BibliographyEntry(doi, apa, bibtex)
workingCache.put(doi, entry)
entry
}

def getBibliographyFromCache(doiLike: String): Option[String] =
def getBibliographyFromCache(doiLike: String): Option[BibliographyEntry] =
extractDoi(doiLike).toOption.flatMap(doi => workingCache.get(doi))

def saveCache(): Unit =
val cacheFile = CiFileCache.getCachedFile("bibliography", "doi-cache.json")
val cache = BibliographyCache(
workingCache
.filter(e => usedDois.contains(e._1))
.map(e => BibliographyEntry(e._1, e._2)).toList
workingCache.filter(e => usedDois.contains(e._1)).values.toList
)
Files.writeString(cacheFile.toPath, cache.asJson.spaces2, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
println(s"Saved bibliography cache with ${cache.entries.size} entries to $cacheFile")
Expand All @@ -78,18 +79,18 @@ object DoiBibliography:
else
Success(split(split.length - 2) + "/" + split.last)

private def fetchBibliography(doi: String)(using as: ActorSystem[_]): Future[String] =
private def fetchBibliography(doi: String, mediaType: MediaType)(using as: ActorSystem[_]): Future[String] =
given ExecutionContext = as.executionContext
val url = s"https://doi.org/$doi"
HttpHelper.getWithFollowRedirects(url, accept = Some(mediaBibliography)) flatMap { response =>
HttpHelper.getWithFollowRedirects(url, accept = Some(MediaRange(mediaType))) flatMap { response =>
if response.status.isSuccess then
response.entity.contentType.mediaType.subType match
case "x-bibliography" =>
case mediaType.subType =>
response.entity.dataBytes.runReduce(_ ++ _).map(_.utf8String)
case _ => Future.failed(
RuntimeException(f"Expected x-bibliography, got ${response.entity.contentType.mediaType}")
RuntimeException(f"Expected $mediaType, got ${response.entity.contentType.mediaType}")
)
else
Future.failed(RuntimeException(f"Failed to fetch bibliography for $doi: ${response.status}"))
Future.failed(RuntimeException(f"Failed to fetch bibliography for $doi with $mediaType: ${response.status}"))
}

0 comments on commit 1fcb0c7

Please sign in to comment.