lyx_mirror/src/insets/InsetListings.cpp
Enrico Forestieri 8dab1cfe7e Fix bug #9101
Update the listings inset to optionally use the minted package
(instead of the listings one) for typesetting code listings.
Only one of the two packages can be used in a document, but it
is possible to switch packages without issues if the used options
are the same. If a switch is made and the options differ, one needs
to manually adjust them if they were entered in the advanced options
tab, or apply again the gui settings.
Note that minted requires the -shell-escape option for the latex
backend and the installation of additional software (python pygments).
2017-06-07 00:55:23 +02:00

526 lines
15 KiB
C++

/**
* \file InsetListings.cpp
* This file is part of LyX, the document processor.
* Licence details can be found in the file COPYING.
*
* \author Bo Peng
* \author Jürgen Spitzmüller
*
* Full author contact details are available in file CREDITS.
*/
#include <config.h>
#include "InsetListings.h"
#include "Buffer.h"
#include "BufferView.h"
#include "BufferParams.h"
#include "Counters.h"
#include "Cursor.h"
#include "DispatchResult.h"
#include "Encoding.h"
#include "FuncRequest.h"
#include "FuncStatus.h"
#include "InsetCaption.h"
#include "Language.h"
#include "LaTeXFeatures.h"
#include "Lexer.h"
#include "output_latex.h"
#include "output_xhtml.h"
#include "OutputParams.h"
#include "TextClass.h"
#include "TexRow.h"
#include "texstream.h"
#include "support/debug.h"
#include "support/docstream.h"
#include "support/gettext.h"
#include "support/lstrings.h"
#include "support/lassert.h"
#include "frontends/alert.h"
#include "frontends/Application.h"
#include "support/regex.h"
#include <sstream>
using namespace std;
using namespace lyx::support;
namespace lyx {
InsetListings::InsetListings(Buffer * buf, InsetListingsParams const & par)
: InsetCaptionable(buf,"listing")
{
params_.setMinted(buffer().params().use_minted);
status_ = par.status();
}
InsetListings::~InsetListings()
{
hideDialogs("listings", this);
}
Inset::DisplayType InsetListings::display() const
{
return params().isInline() || params().isFloat() ? Inline : AlignLeft;
}
void InsetListings::write(ostream & os) const
{
os << "listings" << "\n";
InsetListingsParams const & par = params();
// parameter string is encoded to be a valid lyx token.
string opt = par.encodedString();
if (!opt.empty())
os << "lstparams \"" << opt << "\"\n";
if (par.isInline())
os << "inline true\n";
else
os << "inline false\n";
InsetCaptionable::write(os);
}
void InsetListings::read(Lexer & lex)
{
while (lex.isOK()) {
lex.next();
string token = lex.getString();
if (token == "lstparams") {
lex.next();
string const value = lex.getString();
params().fromEncodedString(value);
} else if (token == "inline") {
lex.next();
params().setInline(lex.getBool());
} else {
// no special option, push back 'status' etc
lex.pushToken(token);
break;
}
}
InsetCaptionable::read(lex);
}
void InsetListings::latex(otexstream & os, OutputParams const & runparams) const
{
string param_string = params().params();
// NOTE: I use {} to quote text, which is an experimental feature
// of the listings package (see page 25 of the manual)
bool const isInline = params().isInline();
bool const use_minted = buffer().params().use_minted;
string minted_language;
string float_placement;
bool const isfloat = params().isFloat();
if (use_minted && (isfloat || contains(param_string, "language="))) {
// Get float placement and/or language of the code,
// then remove the relative options.
vector<string> opts =
getVectorFromString(param_string, ",", false);
for (int i = 0; i < opts.size(); ++i) {
if (prefixIs(opts[i], "float")) {
if (prefixIs(opts[i], "float="))
float_placement = opts[i].substr(6);
opts.erase(opts.begin() + i--);
}
else if (prefixIs(opts[i], "language=")) {
minted_language = opts[i].substr(9);
opts.erase(opts.begin() + i--);
}
}
param_string = getStringFromVector(opts, ",");
}
// Minted needs a language specification
if (minted_language.empty())
minted_language = "TeX";
// get the paragraphs. We can not output them directly to given odocstream
// because we can not yet determine the delimiter character of \lstinline
docstring code;
docstring uncodable;
ParagraphList::const_iterator par = paragraphs().begin();
ParagraphList::const_iterator end = paragraphs().end();
bool encoding_switched = false;
Encoding const * const save_enc = runparams.encoding;
// The listings package cannot deal with multi-byte-encoded
// glyphs, except if full-unicode aware backends
// such as XeTeX or LuaTeX are used, and with pLaTeX.
bool const multibyte_possible = use_minted || runparams.isFullUnicode()
|| (buffer().params().encoding().package() == Encoding::japanese
&& runparams.encoding->package() == Encoding::japanese);
if (!multibyte_possible && !runparams.encoding->hasFixedWidth()) {
// We need to switch to a singlebyte encoding, due to
// the restrictions of the listings package (see above).
// This needs to be consistent with
// LaTeXFeatures::getTClassI18nPreamble().
Language const * const outer_language =
(runparams.local_font != 0) ?
runparams.local_font->language()
: buffer().params().language;
// We try if there's a singlebyte encoding for the current
// language; if not, fall back to latin1.
Encoding const * const lstenc =
(outer_language->encoding()->hasFixedWidth()) ?
outer_language->encoding()
: encodings.fromLyXName("iso8859-1");
switchEncoding(os.os(), buffer().params(), runparams, *lstenc, true);
runparams.encoding = lstenc;
encoding_switched = true;
}
while (par != end) {
pos_type siz = par->size();
bool captionline = false;
for (pos_type i = 0; i < siz; ++i) {
if (i == 0 && par->isInset(i) && i + 1 == siz)
captionline = true;
// ignore all struck out text and (caption) insets
if (par->isDeleted(i)
|| (par->isInset(i) && par->getInset(i)->lyxCode() == CAPTION_CODE))
continue;
if (par->isInset(i)) {
// Currently, this can only be a quote inset
// that is output as plain quote here, but
// we use more generic code anyway.
otexstringstream ots;
OutputParams rp = runparams;
rp.pass_thru = true;
par->getInset(i)->latex(ots, rp);
code += ots.str();
continue;
}
char_type c = par->getChar(i);
// we can only output characters covered by the current
// encoding!
try {
if (runparams.encoding->encodable(c))
code += c;
else if (runparams.dryrun) {
code += "<" + _("LyX Warning: ")
+ _("uncodable character") + " '";
code += docstring(1, c);
code += "'>";
} else
uncodable += c;
} catch (EncodingException & /* e */) {
if (runparams.dryrun) {
code += "<" + _("LyX Warning: ")
+ _("uncodable character") + " '";
code += docstring(1, c);
code += "'>";
} else
uncodable += c;
}
}
++par;
// for the inline case, if there are multiple paragraphs
// they are simply joined. Otherwise, expect latex errors.
if (par != end && !isInline && !captionline)
code += "\n";
}
if (isInline) {
static const docstring delimiters =
from_utf8("!*()-=+|;:'\"`,<.>/?QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm");
size_t pos = delimiters.find_first_not_of(code);
// This code piece contains all possible special character? !!!
// Replace ! with a warning message and use ! as delimiter.
if (pos == string::npos) {
docstring delim_error = "<" + _("LyX Warning: ")
+ _("no more lstline delimiters available") + ">";
code = subst(code, from_ascii("!"), delim_error);
pos = 0;
if (!runparams.dryrun && !runparams.silent) {
// FIXME: warning should be passed to the error dialog
frontend::Alert::warning(_("Running out of delimiters"),
_("For inline program listings, one character must be reserved\n"
"as a delimiter. One of the listings, however, uses all available\n"
"characters, so none is left for delimiting purposes.\n"
"For the time being, I have replaced '!' by a warning, but you\n"
"must investigate!"));
}
}
docstring const delim(1, delimiters[pos]);
if (use_minted) {
os << "\\mintinline";
if (!param_string.empty())
os << "[" << from_utf8(param_string) << "]";
os << "{" << minted_language << "}";
} else {
os << "\\lstinline";
if (!param_string.empty())
os << "[" << from_utf8(param_string) << "]";
else if (pos >= delimiters.find('Q'))
// We need to terminate the command before
// the delimiter
os << " ";
}
os << delim << code << delim;
} else if (use_minted) {
if (isfloat) {
os << breakln << "\\begin{listing}";
if (!float_placement.empty())
os << '[' << float_placement << "]";
}
os << breakln << "\\begin{minted}";
if (!param_string.empty())
os << "[" << param_string << "]";
os << "{" << minted_language << "}\n"
<< code << breakln << "\\end{minted}\n";
if (isfloat) {
OutputParams rp = runparams;
rp.moving_arg = true;
TexString caption = getCaption(rp);
if (!caption.str.empty())
os << "\\caption{" << move(caption) << "}\n";
os << "\\end{listing}\n";
}
} else {
OutputParams rp = runparams;
rp.moving_arg = true;
TexString caption = getCaption(rp);
os << breakln << "\\begin{lstlisting}";
if (param_string.empty() && caption.str.empty())
os << "\n";
else {
if (!runparams.nice)
os << safebreakln;
os << "[";
if (!caption.str.empty()) {
os << "caption={" << move(caption) << '}';
if (!param_string.empty())
os << ',';
}
os << from_utf8(param_string) << "]\n";
}
os << code << breakln << "\\end{lstlisting}\n";
}
if (encoding_switched){
// Switch back
switchEncoding(os.os(), buffer().params(), runparams, *save_enc, true);
runparams.encoding = save_enc;
}
if (!uncodable.empty() && !runparams.silent) {
// issue a warning about omitted characters
// FIXME: should be passed to the error dialog
if (!multibyte_possible && !runparams.encoding->hasFixedWidth())
frontend::Alert::warning(_("Uncodable characters in listings inset"),
bformat(_("The following characters in one of the program listings are\n"
"not representable in the current encoding and have been omitted:\n%1$s.\n"
"This is due to a restriction of the listings package, which does\n"
"not support your encoding '%2$s'.\n"
"Toggling 'Use non-TeX fonts' in Document > Settings...\n"
"might help."),
uncodable, _(runparams.encoding->guiName())));
else
frontend::Alert::warning(_("Uncodable characters in listings inset"),
bformat(_("The following characters in one of the program listings are\n"
"not representable in the current encoding and have been omitted:\n%1$s."),
uncodable));
}
}
docstring InsetListings::xhtml(XHTMLStream & os, OutputParams const & rp) const
{
odocstringstream ods;
XHTMLStream out(ods);
bool const isInline = params().isInline();
if (isInline)
out << html::CompTag("br");
else {
out << html::StartTag("div", "class='float-listings'");
docstring caption = getCaptionHTML(rp);
if (!caption.empty())
out << html::StartTag("div", "class='listings-caption'")
<< XHTMLStream::ESCAPE_NONE
<< caption << html::EndTag("div");
}
InsetLayout const & il = getLayout();
string const & tag = il.htmltag();
string attr = "class ='listings";
string const lang = params().getParamValue("language");
if (!lang.empty())
attr += " " + lang;
attr += "'";
out << html::StartTag(tag, attr);
OutputParams newrp = rp;
newrp.html_disable_captions = true;
// We don't want to convert dashes here. That's the only conversion we
// do for XHTML, so this is safe.
newrp.pass_thru = true;
docstring def = InsetText::insetAsXHTML(out, newrp, InsetText::JustText);
out << html::EndTag(tag);
if (isInline) {
out << html::CompTag("br");
// escaping will already have been done
os << XHTMLStream::ESCAPE_NONE << ods.str();
} else {
out << html::EndTag("div");
// In this case, this needs to be deferred, but we'll put it
// before anything the text itself deferred.
def = ods.str() + '\n' + def;
}
return def;
}
string InsetListings::contextMenuName() const
{
return "context-listings";
}
void InsetListings::doDispatch(Cursor & cur, FuncRequest & cmd)
{
switch (cmd.action()) {
case LFUN_INSET_MODIFY: {
cur.recordUndoInset(this);
InsetListings::string2params(to_utf8(cmd.argument()), params());
break;
}
case LFUN_INSET_DIALOG_UPDATE:
cur.bv().updateDialog("listings", params2string(params()));
break;
default:
InsetCaptionable::doDispatch(cur, cmd);
break;
}
}
bool InsetListings::getStatus(Cursor & cur, FuncRequest const & cmd,
FuncStatus & status) const
{
switch (cmd.action()) {
case LFUN_INSET_MODIFY:
case LFUN_INSET_DIALOG_UPDATE:
status.setEnabled(true);
return true;
case LFUN_CAPTION_INSERT: {
// the inset outputs at most one caption
if (params().isInline() || getCaptionInset()) {
status.setEnabled(false);
return true;
}
}
default:
return InsetCaptionable::getStatus(cur, cmd, status);
}
}
docstring const InsetListings::buttonLabel(BufferView const & bv) const
{
// FIXME UNICODE
if (decoration() == InsetLayout::CLASSIC)
return isOpen(bv) ? _("Listing") : getNewLabel(_("Listing"));
else
return getNewLabel(_("Listing"));
}
void InsetListings::validate(LaTeXFeatures & features) const
{
if (buffer().params().use_minted)
features.require("minted");
else
features.require("listings");
features.useInsetLayout(getLayout());
string param_string = params().params();
if (param_string.find("\\color") != string::npos)
features.require("color");
InsetCaptionable::validate(features);
}
bool InsetListings::showInsetDialog(BufferView * bv) const
{
bv->showDialog("listings", params2string(params()),
const_cast<InsetListings *>(this));
return true;
}
TexString InsetListings::getCaption(OutputParams const & runparams) const
{
InsetCaption const * ins = getCaptionInset();
if (ins == 0)
return TexString();
otexstringstream os;
ins->getArgs(os, runparams);
ins->getArgument(os, runparams);
// TODO: The code below should be moved to support, and then the test
// in ../tests should be moved there as well.
// the caption may contain \label{} but the listings
// package prefer caption={}, label={}
TexString cap = os.release();
if (buffer().params().use_minted
|| !contains(cap.str, from_ascii("\\label{")))
return cap;
// convert from
// blah1\label{blah2} blah3
// to
// blah1 blah3},label={blah2
// to form options
// caption={blah1 blah3},label={blah2}
//
// NOTE that } is not allowed in blah2.
regex const reg("(.*)\\\\label\\{(.*?)\\}(.*)");
string const new_cap("$1$3},label={$2");
// TexString validity: the substitution preserves the number of newlines.
// Moreover we assume that $2 does not contain newlines, so that the texrow
// information remains accurate.
cap.str = from_utf8(regex_replace(to_utf8(cap.str), reg, new_cap));
return cap;
}
void InsetListings::string2params(string const & in,
InsetListingsParams & params)
{
params = InsetListingsParams();
if (in.empty())
return;
istringstream data(in);
Lexer lex;
lex.setStream(data);
// discard "listings", which is only used to determine inset
lex.next();
params.read(lex);
}
string InsetListings::params2string(InsetListingsParams const & params)
{
ostringstream data;
data << "listings" << ' ';
params.write(data);
return data.str();
}
} // namespace lyx