├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── example └── example_stylize.rb ├── lib ├── texstylist.rb └── texstylist │ ├── citations.rb │ ├── csl_adaptor.rb │ ├── csl_constants.rb │ ├── latex_util.rb │ └── unicode_babel.rb ├── test ├── fixtures │ ├── example_bibliography.bib │ ├── example_body.tex │ ├── example_header.tex │ └── example_scholarly_article.yml ├── helper.rb ├── style_hello_world_test.rb └── unicode_babel_test.rb └── texstylist.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | Gemfile.lock 46 | .ruby-version 47 | .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in texstylist.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Deyan Ginev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TeX Stylist 2 | 3 | [Authorea](http://www.authorea.com)'s TeX-based stylist. 4 | 5 | **CAUTION: This repository is in a pre-alpha dev sprint, consider it completely unstable until a 0.1.0 release** 6 | 7 | [![Build Status](https://secure.travis-ci.org/Authorea/texstylist.png?branch=master)](https://travis-ci.org/Authorea/texstylist) 8 | [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/authorea/texstylist/master/LICENSE) 9 | [![Gem Version](https://badge.fury.io/rb/texstylist.svg)](https://badge.fury.io/rb/texstylist) 10 | 11 | ## Common Questions 12 | 13 | **Who is this gem intended for?** Mostly for people working on micro-publication platforms interested in a turnkey solution to customizing the appearance of exported documents. If you're an author you can simply, and freely, use the export features of [Authorea](https://www.authorea.com). 14 | 15 | **Can I directly use it on my LaTeX documents?** Almost. As convention has it with Authorea, you can use your document body directly, but we request that you prepare the document metadata separately, together with the customization parameters. 16 | 17 | We have also released the [texstyles](https://github.com/Authorea/texstyles) Ruby gem, which contains the full list of scholarly styles used at Authorea. We welcome contributions and corrections! 18 | 19 | 20 | ## Usage 21 | 22 | ```ruby 23 | require 'texstylist' 24 | 25 | header = '% A latex preamble, of e.g. custom macro definitions, or custom overrides for the desired style' 26 | abstract = 'An (optional) document abstract' 27 | body = 'An example article body.' 28 | 29 | metadata = { 30 | 'title' => 'An example scholarly article', 31 | 'abstract' => abstract, 32 | # ... full range of scholarly metadata omitted for space 33 | 'bibliography' => 'biblio.bib', 34 | # any bibtex or CSL citation style is accepted 35 | 'citation_style' => 'apacite', 36 | } 37 | 38 | # Any available Style from the texstyles gem is accepted 39 | stylist = Texstylist.new(:authorea) 40 | # A single render call styles the document and citations, typesets the metadata, and handles internationalization 41 | styled_doc = stylist.render(body, header, metadata) 42 | 43 | # Enjoy! 44 | ``` 45 | 46 | You can see a full example [here](https://github.com/Authorea/texstylist/blob/master/example/example_stylize.rb). 47 | 48 | ## Installation 49 | 50 | Add this line to your application's Gemfile: 51 | 52 | ```ruby 53 | gem 'texstylist' 54 | ``` 55 | 56 | And then execute: 57 | 58 | $ bundle 59 | 60 | Or install it yourself as: 61 | 62 | $ gem install texstylist 63 | 64 | ## Roadmap 65 | 66 | ### Supported via [Texstyles](https://github.com/Authorea/texstyles) 67 | * 100+ and growing scholarly export styles 68 | * Core metadata items of scholarly articles 69 | * White/blacklisting LaTeX style and class conflicts 70 | * Independent citation style specifications 71 | 72 | ### Support via [Texstylist](https://github.com/Authorea/texstylist) 73 | * Unicode-only input and output 74 | * Automatic internationalization for LaTeX via babel and pdflatex, by analyzing Unicode locales 75 | * Citation styling API, supporting both [CSL](http://citationstyles.org/) and [bibtex](http://www.bibtex.org/) style files (.bst) 76 | 77 | ### Upcoming 78 | * Use a standard vocabulary and serialization format(s) for scholarly metadata 79 | * Undergo a round of community feedback and evolve the gem respectively 80 | 81 | ## License 82 | 83 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /example/example_stylize.rb: -------------------------------------------------------------------------------- 1 | require 'texstylist' 2 | 3 | header = '% A latex preamble, of e.g. custom macro definitions, or custom overrides for the desired style' 4 | abstract = 'An (optional) document abstract' 5 | body = 'An example article body.' 6 | 7 | metadata = { 8 | 'title' => 'An example scholarly article', 9 | 'short_title' => 'Example article', 10 | 'authors' => [ 11 | { 'name' => 'First Author', 12 | 'affiliation' => 1}, 13 | { 'name' => 'Second Author', 14 | 'affiliation' => 2}, 15 | { 'name' => 'Third Author', 16 | 'affiliations' => [1, 2]} 17 | ], 18 | 'affiliations' => { 19 | 1 => 'Example Organization', 20 | 2 => 'Another Organization' 21 | }, 22 | 'abstract' => abstract 23 | } 24 | 25 | 26 | # Choose any available Texstyles::Style here 27 | stylist = Texstylist.new(:authorea) 28 | 29 | # A single render call styles the document and citations, typesets the metadata, and handles internationalization 30 | stylized_document = stylist.render(body, header, metadata) 31 | 32 | # Enjoy! 33 | puts stylized_document -------------------------------------------------------------------------------- /lib/texstylist.rb: -------------------------------------------------------------------------------- 1 | require 'texstyles' 2 | require 'texstylist/citations' 3 | require 'texstylist/unicode_babel' 4 | 5 | class Texstylist 6 | attr_accessor :style 7 | @@default_package_selection = %w( 8 | graphicx grffile latexsym textcomp longtable multirow booktabs ams natbib url hyperref latexml 9 | inputenc babel) 10 | @@default_package_options = {'grffile' => ['space'], 'inputenc' => ['utf8']} 11 | 12 | def initialize(style = :authorea, package_candidates = @@default_package_selection) 13 | @style = Texstyles::Style.new(style) 14 | # setup default packages 15 | @default_packages_list = package_candidates.select{|candidate| @style.package_compatible?(candidate)} 16 | end 17 | 18 | def render(body, header=nil, metadata = {}) 19 | return '' if body.empty? 20 | @header = header 21 | 22 | # I. Prepare default package inclusions 23 | @default_packages = '' 24 | @default_packages_list.each do |package| 25 | next if @header && @header.match(/\{(?:#{package})\}/) # skip if overridden by the header. 26 | options = @@default_package_options[package] 27 | setup_macro = nil 28 | 29 | # I.1. Expand common aliases, prepare extra setup steps 30 | case package 31 | when 'ams' # alias for a family of packages 32 | package = 'amsfonts,amsmath,amssymb' 33 | when 'hyperref' 34 | setup_macro = "\\hypersetup{colorlinks=false,pdfborder={0 0 0}}" 35 | when 'latexml' 36 | package = nil 37 | setup_macro = "\% You can conditionalize code for latexml or normal latex using this.\n"+ 38 | "\\newif\\iflatexml\\latexmlfalse" 39 | when 'babel' 40 | # handle globally, as we need to automagically internationalize any Unicode 41 | package = nil 42 | end 43 | 44 | # I.2. Add the package inclusion, if any 45 | if package 46 | @default_packages << if options 47 | "\\usepackage[#{options.join(',')}]{#{package}}" 48 | else 49 | "\\usepackage{#{package}}" 50 | end 51 | @default_packages << "\n" 52 | end 53 | # I.3 Add the setup macro, if any 54 | if setup_macro 55 | @default_packages << setup_macro + "\n" 56 | end 57 | end 58 | 59 | # II. Special graceful degradation treatment for common sources of conflicts, done once globally 60 | if !@style.package_compatible?(:natbib) 61 | @default_packages << "\n\\newcommand\\citet{\\cite}\n\\newcommand\\citep{\\cite}" 62 | end 63 | 64 | 65 | # III. Advanced auto-magical internationalization of unicode with babel (intended for use with pdflatex) 66 | if @style.package_compatible?(:babel) 67 | # Having the full body and preamble, figure out which flavours of babel we need (and potentially other text-dependent logic) 68 | metadata["default_packages"] = @default_packages 69 | metadata["header"] = @header 70 | preamble = @style.stylize_metadata(metadata) 71 | # We'll have to rerender the preamble with all language locales setup 72 | @default_packages << UnicodeBabel::latex_inclusions(preamble + body) 73 | @default_packages << "\n" 74 | # And auto-deposit various language activation macros in the article itself 75 | body = UnicodeBabel::activate_foreign_languages(body) 76 | end 77 | 78 | # IV. Render the preamble and prepare the final latex document 79 | metadata["default_packages"] = @default_packages 80 | metadata["header"] = @header 81 | preamble = @style.stylize_metadata(metadata) 82 | article = preamble + "\n\n" + body 83 | 84 | # IV.1. Normalize to simpler latex 85 | article = self.simplify_latex(article) 86 | 87 | # IV.2 Perform citations styling 88 | article = self.stylize_citations(article, metadata) 89 | # IV.3. Wrap up 90 | article << "\n\\end{document}" if @style.package_compatible?(:latex) # finalize latex documents 91 | article << "\n\n" 92 | 93 | return article 94 | end 95 | 96 | def simplify_latex(text) 97 | # \amp can be written as simply \& 98 | text = text.gsub(/\\amp([^\w])/, "\\\\&\\1") 99 | # simplify new line markup if needed 100 | text = text.gsub(/\r\n/, "\n") 101 | end 102 | 103 | def stylize_citations(article, metadata) 104 | Citations::stylize_citations(article, metadata['bibliography'], @style, metadata['citation_style'], decorate: metadata['decorate_citations']) 105 | end 106 | 107 | end -------------------------------------------------------------------------------- /lib/texstylist/citations.rb: -------------------------------------------------------------------------------- 1 | require 'texstylist/csl_adaptor' 2 | class Citations 3 | 4 | # Taken from our Authorea TeX server - texlive 2015 and custom journal styles. 5 | @@citation_styles_bst = Set.new(["ACM-Reference-Format-Journals", "AISB2008", "CUEDbiblio", "ChemCommun", "ChemEurJ", 6 | "Chicago", "IEEEtran", "IEEEtranM", "IEEEtranMN", "IEEEtranN", "IEEEtranS", "IEEEtranSA", "IEEEtranSN", "InorgChem", 7 | "JAmChemSoc", "JAmChemSoc_all", "JHEP", "LHCb", "PhDbiblio-bold", "PhDbiblio-case", "PhDbiblio-url", "PhDbiblio- 8 | url2", "Science", "ScienceAdvances", "UNAMThesis", "aa", "aaai", "aaai-named", "aabbrv", "aalpha", "abbrv", "abbrv- 9 | fr", "abbrv-letters", "abbrvcnb", "abbrvdin", "abbrvhtml", "abbrvnat", "abbrvnat-fr", "abbrvurl", "abntex2-alf", 10 | "abntex2-num", "abstract", "achemso", "achicago", "acl", "acm", "acm-fa", "acm-sigchi", "acmtrans-ims", "address", 11 | "address-html", "address-ldif", "address-vcard", "adfathesis", "adrbirthday", "adrconv", "adrfax", "aer", "aertt", 12 | "agecon", "agsm", "agu", "agu04", "agu08", "agufull", "agufull04", "agufull08", "aiaa", "aichej", "aipauth4-1", 13 | "aipnum4-1", "aj", "ajae", "ajl", "alpha", "alpha-fr", "alpha-letters", "alphadin", "alphahtml", "alphahtmldate", 14 | "alphahtmldater", "alphaurl", "ametsoc", "ametsoc2014", "ams-alph", "ams-pln", "amsalpha", "amsplain", "amsra", 15 | "amsrn", "amsrs", "amsru", "amsry", "angew", "annotate", "annotation", "anotit", "aomalpha", "aomplain", "apa", 16 | "apacann", "apacannx", "apacite", "apacitex", "apalike", "apalike-fr", "apalike-letters", "apalike2", "apanat1b", 17 | "apecon", "apj", "aplain", "apsr", "apsrev", "apsrev4-1", "apsrevM", "apsrmp", "apsrmp4-1", "apsrmpM", "asa-fa", 18 | "asaetr", "ascelike", "asp2010", "astron", "atlasBibStyleWithTitle", "atlasBibStyleWoTitle", "aunsnot", "aunsrt", 19 | "authordate1", "authordate2", "authordate3", "authordate4", "bababbr3", "bababbr3-fl", "bababbr3-lf", "bababbrv", 20 | "bababbrv-fl", "bababbrv-lf", "babalpha", "babalpha-fl", "babalpha-lf", "babamspl", "babplai3", "babplai3-fl", 21 | "babplai3-lf", "babplain", "babplain-fl", "babplain-lf", "babunsrt", "babunsrt-fl", "babunsrt-lf", "bbs", 22 | "besjournals", "bestpapers", "bestpapers-export", "bgteuabbr", "bgteuabbr2", "bgteupln", "bgteupln2", "bgteupln3", 23 | "biblatex", "bibtoref", "biochem", "birthday", "bmc-mathphys", "bookdb", "cascadilla", "cbe", "cc", "cc2", "cell", 24 | "chetref", "chicago", "chicago-annote", "chicago-fa", "chicagoa", "chronological", "chronoplainnm", "chscite", 25 | "cje", "cmpj", "cont-ab", "cont-au", "cont-no", "cont-ti", "copernicus", "cv", "databib", "dcbib", "dcu", "dinat", 26 | "dk-abbrv", "dk-alpha", "dk-apali", "dk-plain", "dk-unsrt", "dlfltxbbibtex", "dtk", "easy", "ecca", "ecta", 27 | "elsarticle-harv", "elsarticle-num", "elsarticle-num-names", "email", "email-html", "en-mtc", "erae", "expcites", 28 | "expkeys", "export", "fbs", "fcavtex", "figbib", "figbib1", "finplain", "fr-mtc", "francais", "francaissc", 29 | "frontiersinMED", "frontiersinMED&FPHY", "frontiersinSCNS&ENG", "frplainnat-letters", "gatech-thesis", "gatech- 30 | thesis-losa", "genetics", "gerabbrv", "geralpha", "gerapali", "gerplain", "gerunsrt", "gji", "glsplain", "glsshort", 31 | "gost2003", "gost2003s", "gost2008", "gost2008l", "gost2008ls", "gost2008n", "gost2008ns", "gost2008s", "gost705", 32 | "gost705s", "gost780", "gost780s", "h-physrev", "hc-de", "hc-en", "humanbio", "humannat", "iclr2015", "ieeepes", 33 | "ieeetr", "ieeetr-fa", "ieeetr-fr", "ier", "ifacconf", "ifacconf-harvard", "ijmart", "ijqc", "imac", "imsart- 34 | nameyear", "imsart-number", "inlinebib", "iopart-num", "is-abbrv", "is-alpha", "is-plain", "is-unsrt", "itaxpf", 35 | "iucr", "jabbrv", "jae", "jalpha", "jas99", "jbact", "jcc", "jfm", "jipsj", "jmb", "jmr", "jname", "jneurosci", 36 | "jorsj", "jox", "jpc", "jpe", "jphysicsB", "jplain", "jponew", "jss2", "jtb", "jthcarsu", "junsrt", "jurabib", 37 | "jurarsp", "jureco", "jurunsrt", "jxb", "klunamed", "klunum", "kluwer", "ksfh_nat", "letter", "listbib", "ltugbib", 38 | "mbplain", "mbunsrtdin", "mdpi", "mn2e", "mnras", "mslapa", "munich", "mybibstyle", "named", "namunsrt", "nar", 39 | "natbib", "natdin", "naturemag", "nddiss2e", "nederlands", "newapa", "newapave", "oega", "ol", "opcit", "osajnl", 40 | "papalike", "pccp", "perception", "phaip", "phapalik", "phcpc", "phiaea", "phjcp", "phnf", "phnflet", "phone", 41 | "phpf", "phppcf", "phreport", "phrmp", "plabbrv", "plain", "plain-fa", "plain-fa-inLTR", "plain-fa-inLTR-beamer", 42 | "plain-fr", "plain-letters", "plainDemo", "plaindin", "plainhtml", "plainhtmldate", "plainhtmldater", "plainnat", 43 | "plainnat-fa", "plainnat-fr", "plainnat-letters", "plainnm", "plainurl", "plainyr", "plalpha", "plos2009", 44 | "plos2015", "plplain", "plunsrt", "pnas", "pnas2009", "psuthesis", "refer", "regstud", "resphilosophica", 45 | "revcompchem", "rsc", "rusnat", "sageep", "sapthesis", "savetrees", "seg", "seuthesis", "siam", "siam-fr", "siam- 46 | letters", "spbasic", "spiebib", "spiejour", "splncs03", "spmpsci", "spphys", "sweabbrv", "swealpha", "sweplain", 47 | "sweplnat", "sweunsrt", "tandfx", "tex-live", "texsis", "thesnumb", "thubib", "tieice", "tipsj", "trb", "tufte", 48 | "udesoftec", "uestcthesis", "ugost2003", "ugost2003s", "ugost2008", "ugost2008l", "ugost2008ls", "ugost2008n", 49 | "ugost2008ns", "ugost2008s", "unified", "unsrt", "unsrt-fa", "unsrt-fr", "unsrtabbrv3", "unsrtdin", "unsrthtml", 50 | "unsrtnat", "unsrtnat-fr", "unsrtnm", "unsrturl", "upmplainnat", "usmeg-a", "usmeg-n", "ussagus", "utphys", "vak", 51 | "vancouver", "worlddev", "xagsm", "xplain", "zharticle"].map{|style| style.to_sym}) 52 | 53 | @@citation_styles_csl = Set.new(CSLAdaptor.list) 54 | 55 | class << self 56 | attr_accessor :citation_styles_bst, :citation_styles_csl 57 | 58 | def stylize_citations(article, bibliography, export_style, citation_style, options = {}) 59 | # nothing to do if no bibliography is given 60 | return article if bibliography.to_s.empty? 61 | 62 | # The citation style we'll use for this export run is either: 63 | citation_style = citation_style.to_s.to_sym 64 | if !(@@citation_styles_csl.member?(citation_style) || @@citation_styles_bst.member?(citation_style)) 65 | # The citation style isn't recognized, use a default for the style - the default for the article would've been passed in 66 | citation_style = export_style.citation_style || # 1. Provided by the export style specification 67 | :plain # 2. The plain citation style as an ultimate fallback 68 | end 69 | @bib_processor = if @@citation_styles_csl.member? citation_style 70 | :citeproc 71 | elsif @@citation_styles_bst.member? citation_style 72 | :bibtex 73 | else 74 | # Fallback to using citeproc processing, as it is faster and simpler 75 | :citeproc 76 | end 77 | 78 | # Bibtex requires some extra latex definitions: 79 | case @bib_processor 80 | when :bibtex 81 | article << "\n\n" 82 | article << "\\bibliographystyle{#{citation_style}}\n" 83 | # TODO: The dfgproposal treatment is needed for any template using BibLaTeX 84 | # for typesetting bibliographies; this is a first of potentially many 85 | article << case export_style.symbol 86 | when :dfgproposal 87 | # The \\printbibliography needs to be in the Bibliography section, which is NOT 88 | # at the end of the article. So we disable it entirely here. 89 | # article << "\\printbibliography\n\n" 90 | "" 91 | when :plos2015 # disable line numbers for PLOS bibliographies 92 | "\\nolinenumbers\n\\bibliography{#{bibliography}}\n\n" 93 | else 94 | "\\bibliography{#{bibliography}}\n\n" 95 | end 96 | when :citeproc 97 | bibtex = begin 98 | BibTeX.open(bibliography) 99 | rescue => e 100 | # TODO: Return errors, without fully failing 101 | puts "Failed to fill in citations due to errors in your Bibliography #{bibliography}: #{e}" 102 | nil 103 | end 104 | # Pandoc can't handle \hyperref links, so don't decorate for the word export. 105 | article = CSLAdaptor.replace_citations_with_csl(article, citation_style, bibtex, decorate: options["decorate"]) 106 | end 107 | 108 | return article 109 | end 110 | 111 | end 112 | end -------------------------------------------------------------------------------- /lib/texstylist/csl_adaptor.rb: -------------------------------------------------------------------------------- 1 | # Motivation for this adaptor can be seen at: https://github.com/inukshuk/csl-ruby/issues/6 2 | require 'citeproc/ruby' 3 | require 'csl/styles' 4 | require 'bibtex' 5 | require 'texstylist/csl_constants' 6 | require 'texstylist/latex_util' 7 | 8 | class CSLAdaptor 9 | 10 | # Borrowing from https://github.com/inukshuk/jekyll-scholar/blob/master/lib/jekyll/scholar/utilities.rb#L5 11 | # until CSL features stabilize 12 | # 13 | # Load styles into static memory. 14 | # They should be thread safe as long as they are 15 | # treated as being read-only. 16 | STYLES = Hash.new do |h, k| 17 | style = CSL::Style.load k 18 | style = style.independent_parent unless style.independent? 19 | h[k.to_s] = style 20 | end 21 | 22 | def self.list 23 | Dir.glob(File.join(CSL::Style.root,'**','*.csl')).map{|p| File.basename(p,".*").to_sym} 24 | end 25 | 26 | def self.safe_style(style) 27 | if style.is_a? Symbol 28 | style = style.to_s 29 | end 30 | style_path = File.basename(style,".*") + '.csl' 31 | expected_path = File.join(CSL::Style.root,style_path) 32 | dependent_path = File.join(CSL::Style.root,'dependent',style_path) 33 | if File.exist?(expected_path) 34 | style 35 | elsif File.exist?(dependent_path) 36 | # While waiting for the main CSL library to implement dependent support, we'll pass the parent here 37 | begin 38 | dom = Nokogiri::XML(File.open(dependent_path)) 39 | parent_link = dom.search('link[@rel="independent-parent"]').first.attr('href') 40 | parent_style = parent_link.sub('http://www.zotero.org/styles/','') 41 | rescue 42 | :'chicago-author-date' 43 | end 44 | else 45 | :'chicago-author-date' 46 | end 47 | end 48 | 49 | def self.load(style) 50 | style = safe_style(style) 51 | begin 52 | style.present? && CSL::Style.load(style) 53 | rescue 54 | nil 55 | end 56 | end 57 | 58 | def self.citation_style_names 59 | CSLConstants.citation_style_names 60 | end 61 | def self.citation_style_symbols 62 | HashWithIndifferentAccess.new(self.citation_style_names.invert) 63 | end 64 | 65 | def self.replace_citations_with_csl(text, citation_style, bibtex, options={}) 66 | options = {decorate: true}.merge(options) 67 | citation_style = CSLAdaptor.safe_style(citation_style) 68 | renderer = CiteProc::Ruby::Renderer.new(format: 'text', style: citation_style) 69 | # Dependent styles still experience issues, use the default chicago processor as a fallback 70 | default_renderer = CiteProc::Ruby::Renderer.new(format: 'text', style: :'chicago-author-date') 71 | 72 | csl_unique_count = 0 73 | csl_map = {} 74 | latex_util = LatexUtil.new 75 | references_section = "\\section*{References}\n" 76 | text = latex_util.preprocess_verb(text) 77 | text = text.gsub(LatexUtil.citation_regex) do |match| 78 | cite_type = $~[:type] 79 | star = $~[:star] 80 | optional_arg1 = $~[:opt1] 81 | optional_arg2 = $~[:opt2] 82 | braces = $~[:braces] 83 | 84 | citations = braces.split(',').flatten 85 | citations = citations.map {|c| c.strip} 86 | length = citations.length 87 | csl_text = citations.map do |c| 88 | new_unique = !csl_map[c] 89 | if new_unique 90 | csl_unique_count += 1 91 | csl_map[c] = csl_unique_count 92 | end 93 | csl_index = csl_map[c] 94 | 95 | bib_data = !c.empty? && bibtex && bibtex[c.to_sym] 96 | if bib_data.nil? # fallback - no such bib entry 97 | '(missing citation)' 98 | else 99 | item = CiteProc::CitationItem.new id: c do |ci| 100 | ci.data = CiteProc::Item.new bib_data.to_citeproc 101 | # numeric styles not yet implemented in citeproc-ruby, so we need to manually set the number, see: 102 | # https://github.com/inukshuk/citeproc-ruby/issues/40 103 | ci.data[:'citation-number'] = csl_index 104 | end 105 | # I. If just added citation, add it to final Bibliography 106 | if new_unique 107 | begin # sometimes the CSL style has no bibliography definition, and the references render raises exceptions 108 | rendered_reference = renderer.render item, STYLES[citation_style].bibliography 109 | if options[:decorate] 110 | references_section << "\\phantomsection\n\\label{csl:#{csl_unique_count}}" 111 | end 112 | references_section << rendered_reference 113 | references_section << "\n\n" 114 | rescue => e 115 | puts "CSL bibliography render failed with: ", e 116 | end 117 | end 118 | 119 | # II. Always add the inline rendered citation 120 | begin 121 | inline_render = renderer.render [item], STYLES[citation_style].citation 122 | if inline_render.blank? 123 | inline_render = begin 124 | default_renderer.render [item], STYLES[citation_style].citation 125 | end 126 | if inline_render.blank? 127 | inline_render = '(missing citation)' 128 | end 129 | end 130 | if options[:decorate] 131 | "\\hyperref[csl:#{csl_index}]{#{inline_render}}" 132 | else 133 | inline_render 134 | end 135 | rescue => e 136 | puts "CSL citation render failed with: ", e 137 | "" 138 | end 139 | end 140 | end 141 | csl_text.join(" ") 142 | end 143 | return latex_util.postprocess_verb(text) + "\n\n" + references_section 144 | end 145 | 146 | 147 | end 148 | -------------------------------------------------------------------------------- /lib/texstylist/latex_util.rb: -------------------------------------------------------------------------------- 1 | class LatexUtil 2 | @inner_chars = "[\\p{word},\\w,\\d,\\-,:,\\.,\\/,%,&,;,\(,\),\\* ]" 3 | @optional_args_chars = "[\\p{word},\\w,\\d,\\-,:,\\.,\\/,%,&,;,\(,\),!,@,\\#,$,\\^,\\*,\\(,\\),<,>,/,\\|,=,_,\\- ]" 4 | @citation_regex = /[\/,\\](?no)?cite(?p|t|al[tp]|NP)?(?\*)?(\[(?#{@optional_args_chars}*)\](\[(?#{@optional_args_chars}*)\])?)?\{(?#{@inner_chars}+)\}/ 5 | 6 | class << self 7 | attr_accessor :citation_regex 8 | end 9 | 10 | def initialize 11 | @verb_store = {} 12 | end 13 | 14 | def preprocess_verb(text) 15 | @verb_store = {} 16 | verb_index = 'a' 17 | # 1. Escape source \verb 18 | text = text.gsub(/\\verb(.)((?:(?!\1).)*)\1/m) do |match| 19 | verb_index << 'a' 20 | key = "aureplacedverb#{verb_index} " 21 | @verb_store[verb_index] = $2 22 | key 23 | end 24 | 25 | # 2. Escape source \begin{verbatim} 26 | text = text.gsub(/\\begin\{verbatim\}(.*?)\\end\{verbatim\}/m) do |match| 27 | verb_index << 'a' 28 | key = "aureplacedverb#{verb_index} " 29 | @verb_store[verb_index] = $1 30 | key 31 | end 32 | 33 | # 3. Escape rendered \verb (as elements) 34 | text = text.gsub(/\(.*?)\<\/code\>/m) do |match| 35 | verb_index << 'a' 36 | key = "aureplacedverb#{verb_index} " 37 | @verb_store[verb_index] = $1 38 | key 39 | end 40 | 41 | return text 42 | end 43 | 44 | def postprocess_verb(text) 45 | return text if @verb_store.empty? 46 | text.gsub(/aureplacedverb(\w+)/) do |match| 47 | "\\verb|"+@verb_store[$1]+"|" 48 | end 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /lib/texstylist/unicode_babel.rb: -------------------------------------------------------------------------------- 1 | module UnicodeBabel 2 | require "stringex" 3 | 4 | class << self # Only class methods, this is a general utility library 5 | def latex_inclusions(string) 6 | locales = babel_locales(string) || [] 7 | locales.push("english") 8 | maybe_fontenc = if locales.include?("russian") || locales.include?("polish") 9 | "\\usepackage[T2A]{fontenc}\n" 10 | else '' 11 | end 12 | maybe_fontenc + "\\usepackage[#{locales.join(",")}]{babel}\n" 13 | end 14 | 15 | def char_declarations(string) 16 | chars = [] 17 | string.chars.uniq.each do |letter| 18 | if locale_to_babel_option(@@locale_map[letter.unpack('U')[0]]) 19 | chars.push(letter) 20 | end 21 | end 22 | chars.map{|c| "\\PrerenderUnicode{#{c}}"}.join("\n") 23 | end 24 | 25 | def babel_locales(string) 26 | language_locales(string).map do |locale| 27 | locale_to_babel_option(locale) 28 | end.flatten.compact 29 | end 30 | def locale_to_babel_option(locale) 31 | case locale 32 | when /latin extended-a/ 33 | # ["czech","dutch","polish","turkish"] 34 | "polish" 35 | when /latin extended-b/ 36 | # ["afrikaans","croatian","serbian","slovene","romanian"] 37 | "romanian" 38 | when /latin-1 supplement/ 39 | "ngerman" 40 | when /cyrillic/ 41 | # ["bulgarian","ukrainian","russian"] 42 | "russian" 43 | when /greek/ 44 | "greek" 45 | when "hangul" 46 | # TODO: \usepackage{xeCJK}, install on server 47 | nil 48 | # ... and so on 49 | when /latin|ascii/ 50 | # "english" -- only if nothing else is found, we'll handle this from latex_inclusions for now 51 | else 52 | nil 53 | end 54 | end 55 | 56 | def language_locales(string) 57 | locales = {} 58 | string.chars.uniq.each do |letter| 59 | locales[@@locale_map[letter.unpack('U')[0]]] = true 60 | end 61 | locales.keys.compact 62 | end 63 | 64 | def activate_foreign_languages(string) 65 | activated_string = "" 66 | open_brace = 0 67 | pending_string = "" 68 | pending_macros = [] 69 | 70 | current_locale = @@locale_map['a'.unpack('U')[0]] # start in English 71 | current_option = "english" 72 | deunicode_mode = false 73 | 74 | string.each_char.with_index do |letter,index| 75 | case letter 76 | when "{" 77 | if string[index-1] != '\\' 78 | if open_brace == 0 79 | # I hate, hate, hate having to do this... but there seems to be no choice 80 | # \macro [ optional stuff ]{many}{mandatory}{arguments}{ <--- we are here? 81 | # ^ 82 | # ^ and we want to insert the select macro prior the first \ 83 | # 84 | # 06/22/2016: EXCEPT in cases like \caption{} inside tables, where babel macros wrapping the \caption will BREAK the alignment magic 85 | # ... and corrupt the entire export. Make sure we stay inside \caption{ ->here<- } 86 | if activated_string.match(/\\caption\s*$/) 87 | activated_string << '{' 88 | else 89 | open_pre = 0 90 | while ((activated_string.length > 0) && (activated_string[-1].match(/^[^\s\\]$/) || (open_pre < 0))) 91 | # Transfer the last letter to the pending stack 92 | last_letter = activated_string[-1] 93 | pending_string.prepend(last_letter) 94 | # Always chop! after using the letter, as single-char strings are passed by reference 95 | activated_string.chop! 96 | 97 | # Modify the pre scope counter if needed 98 | if activated_string[-1] != '\\' 99 | case last_letter 100 | when "{", "[" 101 | open_pre+=1 102 | when "}", "]" 103 | open_pre-=1 104 | end 105 | end 106 | end 107 | # This is either the empty string, a space or a backslash. In each case it's safe to move it over 108 | # but before we do, handle the annoying special case of "\macro {" 109 | activated_string.sub!(/\\(\w+\s*)\z/) do 110 | pending_string.prepend($1) 111 | "\\" 112 | end 113 | last_letter = activated_string[-1] 114 | pending_string.prepend(last_letter) 115 | # Always chop! afterr using the letter, as single-char strings are passed by reference 116 | activated_string.chop! 117 | end 118 | end 119 | open_brace+=1 120 | end 121 | when "}" 122 | if string[index-1] != '\\' 123 | open_brace-=1 124 | 125 | if open_brace == 0 126 | first_select = pending_macros.first.to_s.dup 127 | last_select = pending_macros.last.to_s.dup 128 | if activated_string.match(/\\caption\s*\{$/) # gotta love special cases... 129 | last_select << '}' 130 | end 131 | activated_string << first_select + pending_string + letter + last_select 132 | letter = "" 133 | pending_macros = [] 134 | pending_string = "" 135 | end 136 | end 137 | when /^[^\s,;\.\?!\-\/\:]$/ # don't change locales on safe punctuation 138 | c_locale = @@locale_map[letter.unpack('U')[0]] 139 | if (c_locale != current_locale) 140 | # locale switch, let's update 141 | deunicode_mode = false 142 | babel_option = locale_to_babel_option(c_locale) 143 | select_lang_macro = "" 144 | if babel_option # We know what we are switching to - do so. 145 | select_lang_macro = "\\selectlanguage{#{babel_option}}" 146 | elsif current_option != "english" # We are switching to an unknown mode, so default to English 147 | select_lang_macro = "\\selectlanguage{english}" 148 | babel_option = "english" 149 | else # Default for yet-unsupported by us, or by babel, locales: deunicode to ASCII 150 | deunicode_mode = true 151 | end 152 | # Record the activation outside of any brace scope, if needed 153 | if open_brace > 0 154 | pending_macros.push(select_lang_macro) 155 | else 156 | activated_string << select_lang_macro 157 | end 158 | # Update the current to what we just encountered 159 | if c_locale # but only if the locale is known 160 | current_locale = c_locale 161 | current_option = babel_option 162 | end 163 | end 164 | end 165 | 166 | # Deunicode mode translates each letter downto ascii 167 | if deunicode_mode 168 | letter = letter.to_ascii() 169 | end 170 | # Always record the letter, this is a lossless copy 171 | if open_brace > 0 172 | pending_string << letter 173 | else 174 | activated_string << letter 175 | end 176 | end 177 | activated_string << pending_string # flush any remaining pending 178 | activated_string 179 | end 180 | end 181 | 182 | # We gratefull thank UnicodeScript for inventing these CHARTS for ruby 183 | # Original source: https://github.com/yuri-g/unicode-script/blob/master/lib/unicode_script/charts.rb 184 | CHARTS = { 185 | 'armenian' => (0x0530..0x058f), 186 | 'coptic' => (0x2c80..0x2cff), 187 | 'greek and coptic' => (0x0370..0x03ff), 188 | 'cypriot syllabary' => (0x10800..0x1083f), 189 | 'cyrillic' => (0x0400..0x04ff), 190 | 'cyrillic supplement' => (0x0500..0x052f), 191 | 'cyrillic extended-a' => (0x2de0..0x2dff), 192 | 'cyrillic extended-b' => (0xa640..0xa69f), 193 | 'georgian' => (0x10a0..0x10ff), 194 | 'georgian supplement' => (0x2d00..0x2d2f), 195 | 'hiragana' => (0x3040..0x309f), 196 | 'glagolitic' => (0x2c00..0x2c5f), 197 | 'gothic' => (0x10330..0x1034f), 198 | 'greek extended' => (0x1f00..0x1fff), 199 | 'basic latin' => (0x0000..0x007f), 200 | 'c1 controls and latin-1 supplement' => (0x0080..0x00ff), 201 | 'latin extended-a' => (0x0100..0x017f), 202 | 'latin extended-b' => (0x0180..0x024f), 203 | 'latin extended-c' => (0x2c60..0x2c7f), 204 | 'latin extended-d' => (0xa720..0xa7ff), 205 | 'latin extended additional' => (0x1e00..0x1eff), 206 | 'fullwidth ascii' => (0x0020..0x007e), 207 | 'halfwidth cjk punctuation' => (0x3000..0x303f), 208 | 'halfwidth hangul' => (0x3130..0x318f), 209 | 'linear b syllabary' => (0x10000..0x1007f), 210 | 'linear b ideograms' => (0x10080..0x100ff), 211 | 'ogham' => (0x1680..0x169f), 212 | 'old italic' => (0x10300..0x1032f), 213 | 'phaistos disc' => (0x101d0..0x101ff), 214 | 'runic' => (0x16a0..0x16ff), 215 | 'shavian' => (0x10450..0x1047f), 216 | 'ipa extensions' => (0x0250..0x02af), 217 | 'phonetic extensions' => (0x1d00..0x1d7f), 218 | 'phonetic extensions supplement' => (0x1d80..0x1dbf), 219 | 'modifier tone letters' => (0xa700..0xa71f), 220 | 'spacing modifier letters' => (0x02b0..0x02ff), 221 | 'superscripts and subscripts' => (0x2070..0x209f), 222 | 'combining diacritical marks' => (0x0300..0x036f), 223 | 'combining diacritical marks supplement' => (0x1dc0..0x1dff), 224 | 'combining half marks' => (0xfe20..0xfe2f), 225 | 'bamum' => (0xa6a0..0xa6ff), 226 | 'bamum supplement' => (0x16800..0x16a3f), 227 | 'egyptian hieroglyphs' => (0x13000..0x1342f), 228 | 'ethiopic' => (0x1200..0x137f), 229 | 'ethiopic supplement' => (0x1380..0x139f), 230 | 'ethiopic extended' => (0x2d80..0x2ddf), 231 | 'ethiopic extended-a' => (0xab00..0xab2f), 232 | 'meroitic cursive' => (0x109a0..0x109ff), 233 | 'meroitic hieroglyphs' => (0x10980..0x1099f), 234 | 'nko' => (0x07c0..0x07ff), 235 | 'osmanya' => (0x10480..0x104af), 236 | 'tifinagh' => (0x2d30..0x2d7f), 237 | 'vai' => (0xa500..0xa63f), 238 | 'arabic' => (0x0600..0x06ff), 239 | 'arabic supplement' => (0x0750..0x077f), 240 | 'arabic extended-a' => (0x08a0..0x08ff), 241 | 'arabic presentation forms-a' => (0xfb50..0xfdff), 242 | 'arabic presentation forms-b' => (0xfe70..0xfeff), 243 | 'imperial aramaic' => (0x10840..0x1085f), 244 | 'avestan' => (0x10b00..0x10b3f), 245 | 'carian' => (0x102a0..0x102df), 246 | 'cuneiform' => (0x12000..0x123ff), 247 | 'cuneiform numbers and punctuation' => (0x12400..0x1247f), 248 | 'old persian' => (0x103a0..0x103df), 249 | 'ugaritic' => (0x10380..0x1039f), 250 | 'hebrew' => (0x0590..0x05ff), 251 | 'lycian' => (0x10280..0x1029f), 252 | 'lydian' => (0x10920..0x1093f), 253 | 'mandaic' => (0x0840..0x085f), 254 | 'old south arabian' => (0x10a60..0x10a7f), 255 | 'inscriptional pahlavi' => (0x10b60..0x10b7f), 256 | 'inscriptional parthian' => (0x10b40..0x10b5f), 257 | 'phoenician' => (0x10900..0x1091f), 258 | 'samaritan' => (0x0800..0x083f), 259 | 'syriac' => (0x0700..0x074f), 260 | 'mongolian' => (0x1800..0x18af), 261 | 'old turkic' => (0x10c00..0x10c4f), 262 | 'phags-pa' => (0xa840..0xa87f), 263 | 'tibetan' => (0x0f00..0x0fff), 264 | 'bengali' => (0x0980..0x09ff), 265 | 'brahmi' => (0x11000..0x1107f), 266 | 'chakma' => (0x11100..0x1114f), 267 | 'devanagari' => (0x0900..0x097f), 268 | 'devanagari extended' => (0xa8e0..0xa8ff), 269 | 'gujarati' => (0x0a80..0x0aff), 270 | 'gurmukhi' => (0x0a00..0x0a7f), 271 | 'kaithi' => (0x11080..0x110cf), 272 | 'kannada' => (0x0c80..0x0cff), 273 | 'kharoshthi' => (0x10a00..0x10a5f), 274 | 'lepcha' => (0x1c00..0x1c4f), 275 | 'limbu' => (0x1900..0x194f), 276 | 'malayalam' => (0x0d00..0x0d7f), 277 | 'meetei mayek' => (0xabc0..0xabff), 278 | 'meetei mayek extensions' => (0xaae0..0xaaff), 279 | 'ol chiki' => (0x1c50..0x1c7f), 280 | 'oriya' => (0x0b00..0x0b7f), 281 | 'saurashtra' => (0xa880..0xa8df), 282 | 'sharada' => (0x11180..0x111df), 283 | 'sinhala' => (0x0d80..0x0dff), 284 | 'sora sompeng' => (0x110d0..0x110ff), 285 | 'syloti nagri' => (0xa800..0xa82f), 286 | 'takri' => (0x11680..0x116cf), 287 | 'tamil' => (0x0b80..0x0bff), 288 | 'telugu' => (0x0c00..0x0c7f), 289 | 'thaana' => (0x0780..0x07bf), 290 | 'vedic extensions' => (0x1cd0..0x1cff), 291 | 'balinese' => (0x1b00..0x1b7f), 292 | 'batak' => (0x1bc0..0x1bff), 293 | 'buginese' => (0x1a00..0x1a1f), 294 | 'cham' => (0xaa00..0xaa5f), 295 | 'javanese' => (0xa980..0xa9df), 296 | 'kayah li' => (0xa900..0xa92f), 297 | 'khmer' => (0x1780..0x17ff), 298 | 'khmer symbols' => (0x19e0..0x19ff), 299 | 'lao' => (0x0e80..0x0eff), 300 | 'myanmar' => (0x1000..0x109f), 301 | 'myanmar extended-a' => (0xaa60..0xaa7f), 302 | 'new tai lue' => (0x1980..0x19df), 303 | 'rejang' => (0xa930..0xa95f), 304 | 'sundanese' => (0x1b80..0x1bbf), 305 | 'sundanese supplement' => (0x1cc0..0x1ccf), 306 | 'tai le' => (0x1950..0x197f), 307 | 'tai tham' => (0x1a20..0x1aaf), 308 | 'tai viet' => (0xaa80..0xaadf), 309 | 'thai' => (0x0e00..0x0e7f), 310 | 'buhid' => (0x1740..0x175f), 311 | 'hanunoo' => (0x1720..0x173f), 312 | 'tagalog' => (0x1700..0x171f), 313 | 'tagbanwa' => (0x1760..0x177f), 314 | 'bopomofo' => (0x3100..0x312f), 315 | 'bopomofo extended' => (0x31a0..0x31bf), 316 | 'cjk unified ideographs' => (0x4e00..0x9fcc), 317 | 'cjk unified ideographs extension a' => (0x3400..0x4db5), 318 | 'cjk unified ideographs extension b' => (0x20000..0x2a6d6), 319 | 'cjk unified ideographs extension c' => (0x2a700..0x2b734), 320 | 'cjk unified ideographs extension d' => (0x2b740..0x2b81d), 321 | 'cjk compatibility ideographs' => (0xf900..0xfaff), 322 | 'cjk compatibility ideographs supplement' => (0x2f800..0x2fa1f), 323 | 'kangxi radicals' => (0x2f00..0x2fdf), 324 | 'cjk radicals supplement' => (0x2e80..0x2eff), 325 | 'cjk strokes' => (0x31c0..0x31ef), 326 | 'hangul jamo' => (0x1100..0x11ff), 327 | 'hangul jamo extended-a' => (0xa960..0xa97f), 328 | 'hangul jamo extended-b' => (0xd7b0..0xd7ff), 329 | 'hangul compatibility jamo' => (0x3130..0x318f), 330 | 'katakana' => (0x30a0..0x30ff), 331 | 'katakana phonetic extensions' => (0x31f0..0x31ff), 332 | 'kana supplement' => (0x1b000..0x1b0ff), 333 | 'kanbun' => (0x3190..0x319f), 334 | 'lisu' => (0xa4d0..0xa4ff), 335 | 'miao' => (0x16f00..0x16f9f), 336 | 'yi syllables' => (0xa000..0xa48f), 337 | 'yi radicals' => (0xa490..0xa4cf), 338 | 'cherokee' => (0x13a0..0x13ff), 339 | 'deseret' => (0x10400..0x1044f), 340 | 'unified canadian aboriginal syllabics' => (0x1400..0x167f), 341 | 'unified canadian aboriginal syllabics extended' => (0x18b0..0x18ff) 342 | } 343 | 344 | @@locale_map = CHARTS.map do |key, range| 345 | range.map do |c| 346 | [c, key] 347 | end 348 | end.flatten(1).to_h 349 | 350 | end 351 | -------------------------------------------------------------------------------- /test/fixtures/example_bibliography.bib: -------------------------------------------------------------------------------- 1 | @article{example, 2 | author = {The Author}, 3 | title = {The title of the work}, 4 | journal = {The name of the journal}, 5 | year = 2016, 6 | number = 1, 7 | pages = {1-2}, 8 | month = 9, 9 | note = {An optional note}, 10 | volume = 1 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/example_body.tex: -------------------------------------------------------------------------------- 1 | Hello citations: \cite{example}. -------------------------------------------------------------------------------- /test/fixtures/example_header.tex: -------------------------------------------------------------------------------- 1 | \def\example{example macro} -------------------------------------------------------------------------------- /test/fixtures/example_scholarly_article.yml: -------------------------------------------------------------------------------- 1 | title: 'An example scholarly article' 2 | short_title: 'Example article' 3 | authors: 4 | - name: 'First Author' 5 | affiliation: 1 6 | - name: 'Second Author' 7 | affiliation: 2 8 | - name: 'Third Author' 9 | affiliations: [1, 2] 10 | 11 | affiliations: 12 | 1: 'Example Organization' 13 | 2: 'Another Organization' 14 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'pry' 4 | begin 5 | Bundler.setup(:default, :development) 6 | rescue Bundler::BundlerError => e 7 | $stderr.puts e.message 8 | $stderr.puts "Run `bundle install` to install missing gems" 9 | exit e.status_code 10 | end 11 | require 'minitest/autorun' 12 | require 'minitest/reporters' 13 | 14 | reporter_options = { color: true } 15 | Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(reporter_options)] 16 | 17 | 18 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 19 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 20 | -------------------------------------------------------------------------------- /test/style_hello_world_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'texstylist' 3 | 4 | class StyleHelloWorldTest < Minitest::Test 5 | def setup 6 | @example_metadata = YAML.load(File.read(File.join('test','fixtures','example_scholarly_article.yml'))) 7 | @example_body = File.read(File.join('test','fixtures','example_body.tex')) 8 | @example_bibliography = File.join('test','fixtures','example_bibliography.bib') 9 | end 10 | 11 | def test_can_load_style 12 | stylist = Texstylist.new(:authorea) 13 | style = stylist.style 14 | assert_equal Texstyles::Style, style.class, 'successfully loaded style' 15 | assert_equal "Authorea", style.name, 'successfully loaded style spec' 16 | end 17 | 18 | def test_can_style_hello_world 19 | stylist = Texstylist.new(:authorea) 20 | 21 | latex = 'Hello World!' 22 | styled_latex = stylist.render(latex) 23 | 24 | assert styled_latex.include?(latex), 'content was passed in' 25 | assert styled_latex.match(/\\usepackage\{graphicx\}/), 'default graphicx package is on' 26 | assert styled_latex.match(/\\begin\{document\}/), 'document start exists' 27 | assert styled_latex.match(/\\end\{document\}/), 'document end exists' 28 | end 29 | 30 | def test_can_style_hello_world_with_metadata 31 | stylist = Texstylist.new(:article) 32 | 33 | body = 'Hello \world!' 34 | header = '\def\world{World}' 35 | 36 | styled_doc = stylist.render(body, header, @example_metadata) 37 | 38 | assert styled_doc.include?(body), 'content was passed in' 39 | assert styled_doc.match(/\\usepackage\{graphicx\}/), 'default graphicx package is on' 40 | assert styled_doc.match(/\\begin\{document\}/), 'document start exists' 41 | assert styled_doc.match(/\\end\{document\}/), 'document end exists' 42 | end 43 | 44 | def test_can_auto_internationalize_cyrillic 45 | stylist = Texstylist.new(:article) 46 | 47 | body = 'Hello \world! Здравей свят! Done.' 48 | header = '\def\world{World}' 49 | 50 | styled_doc = stylist.render(body, header, @example_metadata) 51 | 52 | assert styled_doc.include?('Здравей свят'), 'cyrillic passed as is' 53 | assert styled_doc.include?('\\usepackage[russian,english]{babel}') 54 | assert styled_doc.include?('\\selectlanguage{russian}'), 'cyrillic activated' 55 | assert styled_doc.include?('\\selectlanguage{english}'), 'english activated' 56 | end 57 | 58 | def test_can_style_citations_with_csl 59 | metadata = @example_metadata.dup 60 | metadata["bibliography"] = @example_bibliography 61 | 62 | stylist = Texstylist.new(:article) 63 | styled_doc = stylist.render(@example_body, @example_header, metadata) 64 | 65 | assert styled_doc.match(/\(Author 2016\)/), 'inline citations work' 66 | assert styled_doc.match(/\\section\*\{References\}/), 'references section was created' 67 | assert styled_doc.match(/Author, The\. 2016\./), 'references entry has valid author' 68 | assert styled_doc.match(/“The Title of the Work\.”/), 'references entry has valid title' 69 | end 70 | 71 | def test_can_style_citations_with_bibtex 72 | metadata = @example_metadata.dup 73 | metadata["bibliography"] = @example_bibliography 74 | metadata["citation_style"] = "apacite" # simply pick a bibtex citation style 75 | 76 | stylist = Texstylist.new(:article) 77 | styled_doc = stylist.render(@example_body, @example_header, metadata) 78 | 79 | assert styled_doc.match(/\\cite\{example\}/), 'inline citations left as-is' 80 | assert styled_doc.match(/\\bibliographystyle\{apacite\}/), 'citation style was activated' 81 | assert styled_doc.match(/\\bibliography\{[^}]+example_bibliography\.bib\}/), 'bibliography inclusion was added' 82 | end 83 | 84 | def test_readme_example 85 | header = '% A latex preamble, of e.g. custom macro definitions, or custom overrides for the desired style' 86 | abstract = 'An (optional) document abstract' 87 | body = 'An example article body.' 88 | 89 | metadata = { 90 | 'title' => 'An example scholarly article', 91 | 'abstract' => abstract, 92 | # ... full range of scholarly metadata omitted for space 93 | 'bibliography' => 'biblio.bib', 94 | # any bibtex or CSL citation style is accepted 95 | 'citation_style' => 'apacite', 96 | } 97 | 98 | stylist = Texstylist.new(:authorea) # any style from the texstylist gem is accepted 99 | styled_doc = stylist.render(body, header, metadata) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/unicode_babel_test.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'texstylist/unicode_babel' 3 | 4 | class UnicodeBabelTest < Minitest::Test 5 | 6 | def test_correct_latex_inclusions_for_various_Unicode_locales 7 | latex_string = UnicodeBabel.latex_inclusions("testing проба here") 8 | assert_equal "\\usepackage[T2A]{fontenc}\n\\usepackage[russian,english]{babel}\n", latex_string, "correct inclusions for Bulgarian" 9 | 10 | latex_string = UnicodeBabel.latex_inclusions("who is Henri Poincaré ?") 11 | assert_equal "\\usepackage[ngerman,english]{babel}\n", latex_string, "correct inclusions for diacritics" 12 | end 13 | 14 | def test_no_op_on_regular_latex 15 | example = "\\def\\example{\\textbf{macro}}\n \\section{Foo}\n This is an \\textit{\\example} macro.\n" 16 | example_processed = UnicodeBabel.activate_foreign_languages(example) 17 | assert_equal example, example_processed, "No-op on regular latex" 18 | end 19 | 20 | def test_can_handle_unbalanced_latex 21 | example = "\\def\\example{\\textbf{macro}}\n \\section{Foo}\n This is } an \\textit{\\example} macro.\n" 22 | example_processed = UnicodeBabel.activate_foreign_languages(example) 23 | assert_equal example, example_processed, "No-op on regular unbalanced latex" 24 | end 25 | 26 | def test_can_handle_caption_macros 27 | example = 'testing \caption{проба тук} end.' 28 | example_processed = UnicodeBabel.activate_foreign_languages(example) 29 | result_expected = "testing \\caption{\\selectlanguage{russian}{проба тук}\\selectlanguage{russian}} \\selectlanguage{english}end." 30 | assert_equal result_expected, example_processed, "can handle captions" 31 | end 32 | 33 | def test_can_handle_complex_macros 34 | example = 'testing \macro [ optional stuff ]{many}{mandatory}{arguments}{проба тук} end.' 35 | example_processed = UnicodeBabel.activate_foreign_languages(example) 36 | result_expected = "testing \\selectlanguage{russian}\\macro [ optional stuff ]{many}{mandatory}{arguments}{проба тук}\\selectlanguage{russian} \\selectlanguage{english}end." 37 | assert_equal result_expected, example_processed, "can handle complex macros" 38 | end 39 | 40 | def test_no_op_on_english_math 41 | example = '\mathbf{F}_{S-S}(\mathbf{d})&=&\exp\left\{\frac{a_1d^2+a_2d+a_3}{d+a_4}\right\}\mathbf{\hat{d}}\label{eq:interparticle}' 42 | example_processed = UnicodeBabel.activate_foreign_languages(example) 43 | assert_equal example, example_processed, "No-op on english math" 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /texstylist.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | Gem::Specification.new do |spec| 3 | spec.name = "texstylist" 4 | spec.version = "0.0.1" 5 | 6 | spec.authors = ["Deyan Ginev"] 7 | spec.email = ["deyan@authorea.com"] 8 | 9 | spec.summary = %q{Authorea's TeX-based stylist. Think Instagram filters for scholarly documents.} 10 | spec.description = %q{Produces a TeX document from a document+style specification pair. Use with the texstyles gem for easy access to hundreds of styles.} 11 | spec.homepage = "https://github.com/Authorea/texstylist" 12 | spec.license = "MIT" 13 | 14 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 15 | spec.require_paths = ["lib"] 16 | 17 | spec.add_dependency 'escape_utils', '~> 1.2' 18 | spec.add_dependency 'json', '~> 1.8' 19 | spec.add_dependency 'stringex', '~> 2.5' 20 | spec.add_dependency 'texstyles', '~> 0.0.9' 21 | spec.add_dependency 'citeproc-ruby', '1.1.2' 22 | # NOTE: When we bump up the version here, we need to regenerate by hand our lib/csl_constants.rb 23 | spec.add_dependency 'csl-styles', '1.0.1.7' 24 | spec.add_dependency 'bibtex-ruby', '~> 4.2' 25 | 26 | spec.add_development_dependency "bundler", "~> 1.12" 27 | spec.add_development_dependency "pry", "~> 0.10" 28 | spec.add_development_dependency "rake", "~> 10.0" 29 | spec.add_development_dependency "minitest", "~> 5.0" 30 | spec.add_development_dependency "minitest-reporters", "~> 1.1" 31 | 32 | end 33 | --------------------------------------------------------------------------------