Handle BibTeX strings when rendering citations

This commit is contained in:
Patrick Förster 2019-08-30 10:33:54 +02:00
parent 9de957f5bf
commit 9eee9d2d73
19 changed files with 6114 additions and 49 deletions

20
Cargo.lock generated
View file

@ -36,7 +36,7 @@ dependencies = [
[[package]]
name = "ascii"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -155,16 +155,6 @@ dependencies = [
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "citeproc"
version = "0.1.0"
source = "git+https://github.com/latex-lsp/citeproc?rev=695a6205216ee15a4f1f899c03286ce9bddbe993#695a6205216ee15a4f1f899c03286ce9bddbe993"
dependencies = [
"ducc 0.1.0 (git+https://github.com/SkylerLipthay/ducc?rev=558a0ec83cbb8aff57043cfafdb3d728cd509211)",
"html2md 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"once_cell 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "clap"
version = "2.33.0"
@ -197,7 +187,7 @@ name = "combine"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ascii 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1727,12 +1717,13 @@ version = "1.5.0"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"citeproc 0.1.0 (git+https://github.com/latex-lsp/citeproc?rev=695a6205216ee15a4f1f899c03286ce9bddbe993)",
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"copy_dir 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ducc 0.1.0 (git+https://github.com/SkylerLipthay/ducc?rev=558a0ec83cbb8aff57043cfafdb3d728cd509211)",
"futures-boxed 0.1.0",
"futures-preview 0.3.0-alpha.18 (registry+https://github.com/rust-lang/crates.io-index)",
"html2md 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"image 0.22.1 (registry+https://github.com/rust-lang/crates.io-index)",
"indoc 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2249,7 +2240,7 @@ dependencies = [
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum arc-swap 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1507f9b80b3ef096751728cf3f43bb0111ec906e44f5d8587e02c10643b9a2cd"
"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
"checksum ascii 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "91e320562a8fa3286a481b7189f89578ace6b20df99e123c87f2f509c957c5d6"
"checksum ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
"checksum async-datagram 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ec4e1e7c4c16c964d7040b676d2eeb0ce4f94bed2e57e28f28a88768c8f934c"
"checksum async-ready 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9470c79dd71fcd6090676d2ef90f45e9d357831234e8a1f70a1991093019bc27"
"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
@ -2267,7 +2258,6 @@ dependencies = [
"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
"checksum chashmap 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45"
"checksum chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "77d81f58b7301084de3b958691458a53c3f7e0b1d702f77e550b6a88e3a88abe"
"checksum citeproc 0.1.0 (git+https://github.com/latex-lsp/citeproc?rev=695a6205216ee15a4f1f899c03286ce9bddbe993)" = "<none>"
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
"checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"

View file

@ -18,11 +18,12 @@ members = [
[dependencies]
base64 = "0.10.1"
bytes = "0.4.12"
citeproc = { git = "https://github.com/latex-lsp/citeproc", rev = "695a6205216ee15a4f1f899c03286ce9bddbe993" }
clap = "2.33"
copy_dir = "0.1.2"
ducc = { git = "https://github.com/SkylerLipthay/ducc", rev = "558a0ec83cbb8aff57043cfafdb3d728cd509211" }
futures-boxed = { path = "crates/futures_boxed" }
futures-preview = "0.3.0-alpha.18"
html2md = "0.2.9"
image = "0.22.1"
itertools = "0.8.0"
jsonrpc = { path = "crates/jsonrpc" }

8
build.rs Normal file
View file

@ -0,0 +1,8 @@
use std::process::Command;
fn main() {
Command::new("node")
.arg("src/citeproc/js/build.js")
.output()
.expect("Failed to bundle citeproc");
}

View file

@ -1,15 +0,0 @@
use crate::syntax::*;
use lsp_types::{MarkupContent, MarkupKind};
pub fn render_citation(entry_code: &str) -> Option<MarkupContent> {
let tree = BibtexSyntaxTree::from(entry_code);
if tree.entries().iter().any(|entry| entry.fields.is_empty()) {
return None;
}
let markdown = citeproc::render(entry_code)?;
Some(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown.trim().to_owned(),
})
}

15
src/citeproc/js/.babelrc Normal file
View file

@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"ie": "7",
"esmodules": false,
"uglify": true
},
"forceAllTransforms": true
}
]
],
}

93
src/citeproc/js/.gitignore vendored Normal file
View file

@ -0,0 +1,93 @@
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# End of https://www.gitignore.io/api/node
# TypeScript output
out/
# dist
dist/

View file

@ -0,0 +1,5 @@
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all"
}

27
src/citeproc/js/build.js Normal file
View file

@ -0,0 +1,27 @@
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
function shouldBuild() {
if (!fs.existsSync(path.join(__dirname, 'dist', 'citeproc.js'))) {
return true;
}
let mtime = 0;
fs.readdirSync(path.join(__dirname))
.map(fs.lstatSync)
.filter(lstat => lstat.isFile())
.forEach(lstat => {
mtime = Math.max(mtime, lstat.mtimeMs);
});
const lstat = fs.lstatSync(path.join(__dirname, 'dist', 'citeproc.js'));
return mtime > lstat.mtimeMs;
}
if (shouldBuild()) {
cp.execSync('npm install && npm run dist', {
cwd: __dirname,
stdio: 'inherit',
});
}

5763
src/citeproc/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
{
"name": "citeproc",
"version": "0.1.0",
"description": "Render BibTeX citations",
"repository": "https://github.com/latex-lsp/citeproc.git",
"author": "Eric Förster <efoerster@users.noreply.github.com>",
"license": "MIT",
"scripts": {
"dist": "webpack",
"format": "prettier --write \"src/**/*.{js,json}\" \"*.{js,json,yml,md}\" \".vscode/**/*.{json}\""
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@citation-js/core": "^0.4.8",
"@citation-js/plugin-bibtex": "^0.4.8",
"@citation-js/plugin-csl": "^0.4.8",
"@types/node": "^11.13.17",
"@types/webpack": "^4.4.35",
"babel-loader": "^8.0.6",
"babel-polyfill": "^6.26.0",
"null-loader": "^0.1.1",
"prettier": "^1.18.2",
"ts-loader": "^5.4.5",
"ts-node": "^8.3.0",
"tslint": "^5.18.0",
"tslint-config-prettier": "^1.15.0",
"webpack": "^4.35.3",
"webpack-cli": "^3.3.6"
}
}

View file

@ -0,0 +1,13 @@
import { Cite } from '@citation-js/core';
import '@citation-js/plugin-bibtex';
import '@citation-js/plugin-csl';
export default function(code) {
const cite = new Cite(code);
const html = cite.format('bibliography', {
format: 'html',
template: 'apa',
lang: 'en-US',
});
return html;
}

View file

@ -0,0 +1,41 @@
const path = require('path');
const webpack = require('webpack');
const config = {
target: 'web',
entry: ['babel-polyfill', './src/index.js'],
mode: 'production',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'citeproc.js',
library: 'citeproc',
libraryTarget: 'var',
devtoolModuleFilenameTemplate: '../[resource-path]',
},
resolve: {
extensions: ['.js'],
},
plugins: [
new webpack.BannerPlugin({ raw: true, banner: "var console = {};"}),
],
module: {
rules: [
{
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"]
}
},
test: /\.js$/,
},
{
// Map browser dependencies to an empty module
test: /node_modules[/\\](sync-request|isomorphic-fetch|ws)[/\\]/,
use: 'null-loader',
},
],
},
};
module.exports = config;

81
src/citeproc/mod.rs Normal file
View file

@ -0,0 +1,81 @@
use crate::formatting::bibtex::{format_entry, BibtexFormattingParams};
use crate::syntax::*;
use ducc::{Ducc, ExecSettings, Function};
use lsp_types::*;
use once_cell::sync::Lazy;
use std::sync::Mutex;
const JS_CODE: &str = include_str!("js/dist/citeproc.js");
const WRAPPER: &str = "(function execute(code) { return citeproc.default(code); })";
pub struct JavaScriptEngine {
ducc: Ducc,
}
impl JavaScriptEngine {
fn new() -> Self {
let ducc = Ducc::new();
let _: () = ducc.exec(JS_CODE, None, ExecSettings::default()).unwrap();
Self { ducc }
}
pub fn initialize() {
Lazy::force(&ENGINE);
}
}
unsafe impl Send for JavaScriptEngine {}
static ENGINE: Lazy<Mutex<JavaScriptEngine>> = Lazy::new(|| Mutex::new(JavaScriptEngine::new()));
pub fn render_citation(tree: &BibtexSyntaxTree, key: &str) -> Option<MarkupContent> {
let entries = tree.entries();
let entry = entries
.iter()
.find(|entry| entry.key.as_ref().map(BibtexToken::text) == Some(key))?;
if entry.fields.is_empty() {
return None;
}
let entry = replace_strings(tree, &entry);
let entry_code = format_entry(&entry, &BibtexFormattingParams::default());
let engine = ENGINE.lock().unwrap();
let entry_code = engine.ducc.create_string(&entry_code).unwrap();
let func: Function = engine
.ducc
.compile(WRAPPER, None)
.unwrap()
.call(())
.unwrap();
let html: String = func.call((entry_code, ())).ok()?;
let markdown = html2md::parse_html(&html);
Some(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown.trim().to_owned(),
})
}
fn replace_strings(tree: &BibtexSyntaxTree, old_entry: &BibtexEntry) -> BibtexEntry {
let mut strings = Vec::new();
for child in &tree.root.children {
if let BibtexDeclaration::String(string) = &child {
if string.value.is_some() {
strings.push(string);
}
}
}
let mut new_entry = old_entry.clone();
for field in &mut new_entry.fields {
if let Some(BibtexContent::Word(reference)) = &field.content {
if let Some(string) = strings
.iter()
.find(|string| string.name.as_ref().unwrap().text() == reference.token.text())
{
field.content = string.value.clone();
}
}
}
new_entry
}

View file

@ -25,7 +25,7 @@ pub enum CompletionItemData {
Class,
EntryType,
FieldName,
Citation { entry_code: String },
Citation { uri: Uri, key: String },
Argument,
}
@ -260,6 +260,7 @@ pub fn class(
pub fn citation(
request: &FeatureRequest<CompletionParams>,
uri: Uri,
entry: &BibtexEntry,
key: String,
text_edit: TextEdit,
@ -282,10 +283,10 @@ pub fn citation(
);
CompletionItem {
label: key,
label: key.to_owned(),
kind: Some(adjust_kind(request, CompletionItemKind::Field)),
filter_text: Some(filter_text),
data: Some(CompletionItemData::Citation { entry_code }.into()),
data: Some(CompletionItemData::Citation { uri, key }.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}

View file

@ -29,7 +29,13 @@ impl FeatureProvider for LatexCitationCompletionProvider {
if let Some(key) = &entry.key {
let key = key.text().to_owned();
let text_edit = TextEdit::new(context.range, key.clone());
let item = factory::citation(request, entry, key, text_edit);
let item = factory::citation(
request,
document.uri.clone(),
entry,
key,
text_edit,
);
items.push(item);
}
}

View file

@ -1,5 +1,4 @@
use crate::citeproc::render_citation;
use crate::formatting::bibtex::{self, BibtexFormattingParams};
use crate::range::RangeExt;
use crate::syntax::*;
use crate::workspace::*;
@ -19,18 +18,18 @@ impl FeatureProvider for LatexCitationHoverProvider {
&'a self,
request: &'a FeatureRequest<TextDocumentPositionParams>,
) -> Option<Hover> {
let entry = Self::get_entry(request)?;
let (tree, entry) = Self::get_entry(request)?;
if entry.is_comment() {
None
} else {
let entry_code = bibtex::format_entry(&entry, &BibtexFormattingParams::default());
match render_citation(&entry_code) {
let key = entry.key.as_ref().unwrap().text();
match render_citation(&tree, key) {
Some(markdown) => Some(Hover {
contents: HoverContents::Markup(markdown),
range: None,
}),
None => {
warn!("Failed to render entry:\n{}", &entry_code);
warn!("Failed to render entry: {}", key);
None
}
}
@ -39,14 +38,16 @@ impl FeatureProvider for LatexCitationHoverProvider {
}
impl LatexCitationHoverProvider {
fn get_entry(request: &FeatureRequest<TextDocumentPositionParams>) -> Option<&BibtexEntry> {
fn get_entry(
request: &FeatureRequest<TextDocumentPositionParams>,
) -> Option<(&BibtexSyntaxTree, &BibtexEntry)> {
let key = Self::get_key(request)?;
for document in request.related_documents() {
if let SyntaxTree::Bibtex(tree) = &document.tree {
for entry in tree.entries() {
if let Some(current_key) = &entry.key {
if current_key.text() == key {
return Some(entry);
return Some((tree, entry));
}
}
}

View file

@ -14,10 +14,7 @@ use tokio_codec::FramedWrite;
#[runtime::main(runtime_tokio::Tokio)]
async fn main() {
// Force initialization of the underlying JavaScript engine
// to decrease the time of the initial citation rendering
drop(texlab::citeproc::render_citation(""));
texlab::citeproc::JavaScriptEngine::initialize();
let matches = app_from_crate!()
.author("")
.arg(

View file

@ -242,8 +242,14 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
.documentation(&item.label)
.map(Documentation::MarkupContent);
}
CompletionItemData::Citation { entry_code } => {
item.documentation = render_citation(&entry_code).map(Documentation::MarkupContent)
CompletionItemData::Citation { uri, key } => {
let workspace = self.workspace_manager.get();
if let Some(document) = workspace.find(&uri) {
if let SyntaxTree::Bibtex(tree) = &document.tree {
let markup = render_citation(&tree, &key);
item.documentation = markup.map(Documentation::MarkupContent);
}
}
}
_ => {}
};

View file

@ -1,10 +1,11 @@
use lsp_types::*;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::path::Path;
#[derive(Debug, Clone, Eq)]
#[derive(Debug, Eq, Clone, Serialize, Deserialize)]
pub struct Uri(Url);
impl Uri {