/** * \file Buffer.cpp * This file is part of LyX, the document processor. * Licence details can be found in the file COPYING. * * \author Lars Gullik Bjønnes * \author Stefan Schimanski * * Full author contact details are available in file CREDITS. */ #include #include "Buffer.h" #include "Author.h" #include "BiblioInfo.h" #include "BranchList.h" #include "buffer_funcs.h" #include "BufferList.h" #include "BufferParams.h" #include "Bullet.h" #include "Chktex.h" #include "ColorSet.h" #include "Converter.h" #include "Counters.h" #include "Cursor.h" #include "CutAndPaste.h" #include "DispatchResult.h" #include "DocIterator.h" #include "BufferEncodings.h" #include "ErrorList.h" #include "Exporter.h" #include "Format.h" #include "FuncRequest.h" #include "FuncStatus.h" #include "IndicesList.h" #include "InsetIterator.h" #include "InsetList.h" #include "Language.h" #include "LaTeXFeatures.h" #include "LaTeX.h" #include "Layout.h" #include "Lexer.h" #include "LyXAction.h" #include "LyX.h" #include "LyXRC.h" #include "LyXVC.h" #include "output.h" #include "output_latex.h" #include "output_docbook.h" #include "output_plaintext.h" #include "output_xhtml.h" #include "Paragraph.h" #include "ParagraphParameters.h" #include "ParIterator.h" #include "PDFOptions.h" #include "Session.h" #include "SpellChecker.h" #include "xml.h" #include "texstream.h" #include "TexRow.h" #include "Text.h" #include "TextClass.h" #include "TocBackend.h" #include "Undo.h" #include "VCBackend.h" #include "version.h" #include "WordLangTuple.h" #include "insets/InsetBranch.h" #include "insets/InsetInclude.h" #include "insets/InsetText.h" #include "mathed/InsetMathHull.h" #include "mathed/MacroTable.h" #include "mathed/InsetMathMacroTemplate.h" #include "mathed/MathSupport.h" #include "graphics/PreviewLoader.h" #include "frontends/Application.h" #include "frontends/alert.h" #include "frontends/Delegates.h" #include "frontends/WorkAreaManager.h" #include "support/lassert.h" #include "support/convert.h" #include "support/debug.h" #include "support/docstring_list.h" #include "support/ExceptionMessage.h" #include "support/FileMonitor.h" #include "support/FileName.h" #include "support/FileNameList.h" #include "support/filetools.h" #include "support/gettext.h" #include "support/gzstream.h" #include "support/lstrings.h" #include "support/mutex.h" #include "support/os.h" #include "support/Package.h" #include "support/PathChanger.h" #include "support/Systemcall.h" #include "support/TempFile.h" #include "support/textutils.h" #include "support/types.h" #include #include #include #include #include #include #include #include using namespace std; using namespace lyx::support; using namespace lyx::graphics; namespace lyx { namespace Alert = frontend::Alert; namespace os = support::os; namespace { int const LYX_FORMAT = LYX_FORMAT_LYX; typedef map DepClean; // Information about labels and their associated refs struct LabelInfo { /// label string docstring label; /// label inset InsetLabel const * inset; /// associated references cache Buffer::References references; /// whether this label is active (i.e., not deleted) bool active; }; typedef vector LabelCache; typedef map RefCache; // A storehouse for the cloned buffers. typedef list CloneStore; CloneStore cloned_buffers; } // namespace class Buffer::Impl { public: Impl(Buffer * owner, FileName const & file, bool readonly, Buffer const * cloned_buffer); ~Impl() { delete preview_loader_; if (wa_) { wa_->closeAll(); delete wa_; } delete inset; } /// search for macro in local (buffer) table or in children MacroData const * getBufferMacro(docstring const & name, DocIterator const & pos) const; /// Update macro table starting with position of it \param it in some /// text inset. void updateMacros(DocIterator & it, DocIterator & scope); /// void setLabel(ParIterator & it, UpdateType utype) const; /** If we have branches that use the file suffix feature, return the file name with suffix appended. */ FileName exportFileName() const; Buffer * owner_; BufferParams params; LyXVC lyxvc; FileName temppath; mutable TexRow texrow; /// need to regenerate .tex? DepClean dep_clean; /// name of the file the buffer is associated with. FileName filename; /// mutable TocBackend toc_backend; /// macro tables struct ScopeMacro { ScopeMacro() {} ScopeMacro(DocIterator const & s, MacroData const & m) : scope(s), macro(m) {} DocIterator scope; MacroData macro; }; typedef map PositionScopeMacroMap; typedef map NamePositionScopeMacroMap; /// map from the macro name to the position map, /// which maps the macro definition position to the scope and the MacroData. NamePositionScopeMacroMap macros; /// positions of child buffers in the buffer typedef map BufferPositionMap; struct ScopeBuffer { ScopeBuffer() : buffer(nullptr) {} ScopeBuffer(DocIterator const & s, Buffer const * b) : scope(s), buffer(b) {} DocIterator scope; Buffer const * buffer; }; typedef map PositionScopeBufferMap; /// position of children buffers in this buffer BufferPositionMap children_positions; /// map from children inclusion positions to their scope and their buffer PositionScopeBufferMap position_to_children; /// Contains the old buffer filePath() while saving-as, or the /// directory where the document was last saved while loading. string old_position; /** Keeps track of the path of local layout files. * If possible, it is always relative to the buffer path. * Empty for layouts in system or user directory. */ string layout_position; /// Container for all sort of Buffer dependant errors. map errorLists; /// checksum used to test if the file has been externally modified. Used to /// double check whether the file had been externally modified when saving. unsigned long checksum_; /// frontend::WorkAreaManager * wa_; /// frontend::GuiBufferDelegate * gui_; /// Undo undo_; /// A cache for the bibfiles (including bibfiles of loaded child /// documents), needed for appropriate update of natbib labels. mutable docstring_list bibfiles_cache_; // FIXME The caching mechanism could be improved. At present, we have a // cache for each Buffer, that caches all the bibliography info for that // Buffer. A more efficient solution would be to have a global cache per // file, and then to construct the Buffer's bibinfo from that. /// A cache for bibliography info mutable BiblioInfo bibinfo_; /// Cache of timestamps of .bib files map bibfile_status_; /// These two hold the file name and format, written to by /// Buffer::preview and read from by LFUN_BUFFER_VIEW_CACHE. FileName preview_file_; string preview_format_; /// Cache the references associated to a label and their positions /// in the buffer. mutable RefCache ref_cache_; /// Cache the label insets and their activity status. mutable LabelCache label_cache_; /// our Text that should be wrapped in an InsetText InsetText * inset; /// PreviewLoader * preview_loader_; /// If non zero, this buffer is a clone of existing buffer \p cloned_buffer_ /// This one is useful for preview detached in a thread. Buffer const * cloned_buffer_; /// CloneList_ptr clone_list_; /// std::list include_list_; private: /// So we can force access via the accessors. mutable Buffer const * parent_buffer; FileMonitorPtr file_monitor_; /// ints and bools are all listed last so as to avoid alignment issues public: /// original format of loaded file int file_format; /// are we in the process of exporting this buffer? mutable bool doing_export; /// If there was an error when previewing, on the next preview we do /// a fresh compile (e.g. in case the user installed a package that /// was missing). bool require_fresh_start_; /// Indicates whether the bibinfo has changed since the last time /// we ran updateBuffer(), i.e., whether citation labels may need /// to be updated. mutable bool cite_labels_valid_; /// Do we have a bibliography environment? mutable bool have_bibitems_; /// is save needed? mutable bool lyx_clean; /// is autosave needed? mutable bool bak_clean; /// is this an unnamed file (New...)? bool unnamed; /// is this an internal bufffer? bool internal_buffer; /// buffer is r/o bool read_only; /** Set to true only when the file is fully loaded. * Used to prevent the premature generation of previews * and by the citation inset. */ bool file_fully_loaded; /// if the file was originally loaded from an older format, do /// we need to back it up still? bool need_format_backup; /// Ignore the parent (e.g. when exporting a child standalone)? bool ignore_parent; /// This seem to change the way Buffer::getMacro() works mutable bool macro_lock; /// has been externally modified? Can be reset by the user. mutable bool externally_modified_; /// whether the bibinfo cache is valid mutable bool bibinfo_cache_valid_; /// mutable bool need_update; private: int word_count_; int char_count_; int blank_count_; public: /// This is here to force the test to be done whenever parent_buffer /// is accessed. Buffer const * parent() const { // ignore_parent temporarily "orphans" a buffer // (e.g. if a child is compiled standalone) if (ignore_parent) return nullptr; // if parent_buffer is not loaded, then it has been unloaded, // which means that parent_buffer is an invalid pointer. So we // set it to null in that case. // however, the BufferList doesn't know about cloned buffers, so // they will always be regarded as unloaded. in that case, we hope // for the best. if (!cloned_buffer_ && !theBufferList().isLoaded(parent_buffer)) parent_buffer = nullptr; return parent_buffer; } /// void setParent(Buffer const * pb) { if (parent_buffer == pb) // nothing to do return; if (!cloned_buffer_ && parent_buffer && pb) LYXERR0("Warning: a buffer should not have two parents!"); parent_buffer = pb; if (!cloned_buffer_ && parent_buffer) parent_buffer->invalidateBibinfoCache(); } /// compute statistics /// \p from initial position /// \p to points to the end position void updateStatistics(DocIterator & from, DocIterator & to, bool skipNoOutput = true); /// statistics accessor functions int wordCount() const { return word_count_; } int charCount(bool with_blanks) const { return char_count_ + (with_blanks ? blank_count_ : 0); } // Make sure the file monitor monitors the good file. void refreshFileMonitor(); /// Notify or clear of external modification void fileExternallyModified(bool exists); ///Binding LaTeX lines with buffer positions. //Common routine for LaTeX and Reference errors listing. void traverseErrors(TeXErrors::Errors::const_iterator err, TeXErrors::Errors::const_iterator end, ErrorList & errorList) const; }; /// Creates the per buffer temporary directory static FileName createBufferTmpDir() { // FIXME This would be the ideal application for a TempDir class (like // TempFile but for directories) string counter; { static int count; static Mutex mutex; Mutex::Locker locker(&mutex); counter = convert(count++); } // We are in our own directory. Why bother to mangle name? // In fact I wrote this code to circumvent a problematic behaviour // (bug?) of EMX mkstemp(). FileName tmpfl(package().temp_dir().absFileName() + "/lyx_tmpbuf" + counter); if (!tmpfl.createDirectory(0777)) { throw ExceptionMessage(WarningException, _("Disk Error: "), bformat( _("LyX could not create the temporary directory '%1$s' (Disk is full maybe?)"), from_utf8(tmpfl.absFileName()))); } return tmpfl; } Buffer::Impl::Impl(Buffer * owner, FileName const & file, bool readonly_, Buffer const * cloned_buffer) : owner_(owner), filename(file), toc_backend(owner), checksum_(0), wa_(nullptr), gui_(nullptr), undo_(*owner), inset(nullptr), preview_loader_(nullptr), cloned_buffer_(cloned_buffer), clone_list_(nullptr), parent_buffer(nullptr), file_format(LYX_FORMAT), doing_export(false), require_fresh_start_(false), cite_labels_valid_(false), have_bibitems_(false), lyx_clean(true), bak_clean(true), unnamed(false), internal_buffer(false), read_only(readonly_), file_fully_loaded(false), need_format_backup(false), ignore_parent(false), macro_lock(false), externally_modified_(false), bibinfo_cache_valid_(false), need_update(false), word_count_(0), char_count_(0), blank_count_(0) { refreshFileMonitor(); if (!cloned_buffer_) { temppath = createBufferTmpDir(); lyxvc.setBuffer(owner_); Language const * inplang = theApp() ? languages.getFromCode(theApp()->inputLanguageCode()) : nullptr; if (inplang) params.language = inplang; if (use_gui) wa_ = new frontend::WorkAreaManager; return; } temppath = cloned_buffer_->d->temppath; file_fully_loaded = true; params = cloned_buffer_->d->params; bibfiles_cache_ = cloned_buffer_->d->bibfiles_cache_; bibinfo_ = cloned_buffer_->d->bibinfo_; bibinfo_cache_valid_ = cloned_buffer_->d->bibinfo_cache_valid_; bibfile_status_ = cloned_buffer_->d->bibfile_status_; cite_labels_valid_ = cloned_buffer_->d->cite_labels_valid_; have_bibitems_ = cloned_buffer_->d->have_bibitems_; unnamed = cloned_buffer_->d->unnamed; internal_buffer = cloned_buffer_->d->internal_buffer; layout_position = cloned_buffer_->d->layout_position; preview_file_ = cloned_buffer_->d->preview_file_; preview_format_ = cloned_buffer_->d->preview_format_; require_fresh_start_ = cloned_buffer_->d->require_fresh_start_; } Buffer::Buffer(string const & file, bool readonly, Buffer const * cloned_buffer) : d(new Impl(this, FileName(file), readonly, cloned_buffer)) { LYXERR(Debug::INFO, "Buffer::Buffer()"); if (cloned_buffer) { d->inset = new InsetText(*cloned_buffer->d->inset); d->inset->setBuffer(*this); // FIXME: optimize this loop somewhat, maybe by creating a new // general recursive Inset::setId(). DocIterator it = doc_iterator_begin(this); DocIterator cloned_it = doc_iterator_begin(cloned_buffer); for (; !it.atEnd(); it.forwardPar(), cloned_it.forwardPar()) it.paragraph().setId(cloned_it.paragraph().id()); } else d->inset = new InsetText(this); d->inset->getText(0)->setMacrocontextPosition(par_iterator_begin()); } Buffer::~Buffer() { LYXERR(Debug::INFO, "Buffer::~Buffer()"); // here the buffer should take care that it is // saved properly, before it goes into the void. // GuiView already destroyed d->gui_ = nullptr; if (isInternal()) { // No need to do additional cleanups for internal buffer. delete d; return; } if (isClone()) { // this is in case of recursive includes: we won't try to delete // ourselves as a child. d->clone_list_->erase(this); // loop over children for (auto const & p : d->children_positions) { Buffer * child = const_cast(p.first); if (d->clone_list_->erase(child)) delete child; } // if we're the master buffer, then we should get rid of the list // of clones if (!parent()) { // If this is not empty, we have leaked something. Worse, one of the // children still has a reference to this list. But we will try to // continue, rather than shut down. LATTEST(d->clone_list_->empty()); // The clone list itself is empty, but it's still referenced in our list // of clones. So let's find it and remove it. CloneStore::iterator it = find(cloned_buffers.begin(), cloned_buffers.end(), d->clone_list_); if (it == cloned_buffers.end()) { // We will leak in this case, but it is safe to continue. LATTEST(false); } else cloned_buffers.erase(it); } // FIXME Do we really need to do this right before we delete d? // clear references to children in macro tables d->children_positions.clear(); d->position_to_children.clear(); } else { // loop over children for (auto const & p : d->children_positions) { Buffer * child = const_cast(p.first); if (theBufferList().isLoaded(child)) { if (theBufferList().isOthersChild(this, child)) child->setParent(nullptr); else theBufferList().release(child); } } if (!isClean()) { docstring msg = _("LyX attempted to close a document that had unsaved changes!\n"); try { msg += emergencyWrite(); } catch (...) { msg += " " + _("Save failed! Document is lost."); } Alert::warning(_("Attempting to close changed document!"), msg); } // FIXME Do we really need to do this right before we delete d? // clear references to children in macro tables d->children_positions.clear(); d->position_to_children.clear(); if (!d->temppath.destroyDirectory()) { LYXERR0(bformat(_("Could not remove the temporary directory %1$s"), from_utf8(d->temppath.absFileName()))); } removePreviews(); } delete d; } Buffer * Buffer::cloneWithChildren() const { BufferMap bufmap; cloned_buffers.emplace_back(new CloneList); CloneList_ptr clones = cloned_buffers.back(); cloneWithChildren(bufmap, clones); // make sure we got cloned BufferMap::const_iterator bit = bufmap.find(this); LASSERT(bit != bufmap.end(), return nullptr); Buffer * cloned_buffer = bit->second; return cloned_buffer; } void Buffer::cloneWithChildren(BufferMap & bufmap, CloneList_ptr clones) const { // have we already been cloned? if (bufmap.find(this) != bufmap.end()) return; Buffer * buffer_clone = new Buffer(fileName().absFileName(), false, this); // The clone needs its own DocumentClass, since running updateBuffer() will // modify it, and we would otherwise be sharing it with the original Buffer. buffer_clone->params().makeDocumentClass(true); cap::switchBetweenClasses( params().documentClassPtr(), buffer_clone->params().documentClassPtr(), static_cast(buffer_clone->inset())); bufmap[this] = buffer_clone; clones->insert(buffer_clone); buffer_clone->d->clone_list_ = clones; buffer_clone->d->macro_lock = true; buffer_clone->d->children_positions.clear(); // FIXME (Abdel 09/01/2010): this is too complicated. The whole children_positions and // math macro caches need to be rethought and simplified. // I am not sure wether we should handle Buffer cloning here or in BufferList. // Right now BufferList knows nothing about buffer clones. for (auto const & p : d->position_to_children) { DocIterator dit = p.first.clone(buffer_clone); dit.setBuffer(buffer_clone); Buffer * child = const_cast(p.second.buffer); child->cloneWithChildren(bufmap, clones); BufferMap::iterator const bit = bufmap.find(child); LASSERT(bit != bufmap.end(), continue); Buffer * child_clone = bit->second; Inset * inset = dit.nextInset(); LASSERT(inset && inset->lyxCode() == INCLUDE_CODE, continue); InsetInclude * inset_inc = static_cast(inset); inset_inc->setChildBuffer(child_clone); child_clone->d->setParent(buffer_clone); // FIXME Do we need to do this now, or can we wait until we run updateMacros()? buffer_clone->setChild(dit, child_clone); } buffer_clone->d->macro_lock = false; } Buffer * Buffer::cloneBufferOnly() const { cloned_buffers.emplace_back(new CloneList); CloneList_ptr clones = cloned_buffers.back(); Buffer * buffer_clone = new Buffer(fileName().absFileName(), false, this); // The clone needs its own DocumentClass, since running updateBuffer() will // modify it, and we would otherwise be sharing it with the original Buffer. buffer_clone->params().makeDocumentClass(true); cap::switchBetweenClasses( params().documentClassPtr(), buffer_clone->params().documentClassPtr(), static_cast(buffer_clone->inset())); clones->insert(buffer_clone); buffer_clone->d->clone_list_ = clones; // we won't be cloning the children buffer_clone->d->children_positions.clear(); return buffer_clone; } bool Buffer::isClone() const { return d->cloned_buffer_; } void Buffer::changed(bool update_metrics) const { if (d->wa_) d->wa_->redrawAll(update_metrics); } frontend::WorkAreaManager & Buffer::workAreaManager() const { LBUFERR(d->wa_); return *d->wa_; } Text & Buffer::text() const { return d->inset->text(); } Inset & Buffer::inset() const { return *d->inset; } BufferParams & Buffer::params() { return d->params; } BufferParams const & Buffer::params() const { return d->params; } BufferParams const & Buffer::masterParams() const { if (masterBuffer() == this) return params(); BufferParams & mparams = const_cast(masterBuffer())->params(); // Copy child authors to the params. We need those pointers. for (Author const & a : params().authors()) mparams.authors().record(a); return mparams; } double Buffer::fontScalingFactor() const { return isExporting() ? 75.0 * params().html_math_img_scale : 0.01 * lyxrc.dpi * lyxrc.currentZoom * lyxrc.preview_scale_factor * params().display_pixel_ratio; } ParagraphList & Buffer::paragraphs() { return text().paragraphs(); } ParagraphList const & Buffer::paragraphs() const { return text().paragraphs(); } LyXVC & Buffer::lyxvc() { return d->lyxvc; } LyXVC const & Buffer::lyxvc() const { return d->lyxvc; } string const Buffer::temppath() const { return d->temppath.absFileName(); } TexRow & Buffer::texrow() { return d->texrow; } TexRow const & Buffer::texrow() const { return d->texrow; } TocBackend & Buffer::tocBackend() const { return d->toc_backend; } Undo & Buffer::undo() { return d->undo_; } Undo const & Buffer::undo() const { return d->undo_; } void Buffer::setChild(DocIterator const & dit, Buffer * child) { d->children_positions[child] = dit; } string Buffer::latexName(bool const no_path) const { FileName latex_name = makeLatexName(d->exportFileName()); return no_path ? latex_name.onlyFileName() : latex_name.absFileName(); } FileName Buffer::Impl::exportFileName() const { docstring const branch_suffix = params.branchlist().getFileNameSuffix(); if (branch_suffix.empty()) return filename; string const name = addExtension(filename.onlyFileNameWithoutExt() + to_utf8(branch_suffix), filename.extension()); FileName res(filename.onlyPath().absFileName() + "/" + name); return res; } string Buffer::logName(LogType * type) const { string const filename = latexName(false); if (filename.empty()) { if (type) *type = latexlog; return string(); } string const path = temppath(); FileName const fname(addName(temppath(), onlyFileName(changeExtension(filename, ".log")))); // FIXME: how do we know this is the name of the build log? FileName const bname( addName(path, onlyFileName( changeExtension(filename, theFormats().extension(params().bufferFormat()) + ".out")))); // Also consider the master buffer log file FileName masterfname = fname; LogType mtype = latexlog; if (masterBuffer() != this) { string const mlogfile = masterBuffer()->logName(&mtype); masterfname = FileName(mlogfile); } // If no Latex log or Build log is newer, show Build log if (bname.exists() && ((!fname.exists() && !masterfname.exists()) || (fname.lastModified() < bname.lastModified() && masterfname.lastModified() < bname.lastModified()))) { LYXERR(Debug::FILES, "Log name calculated as: " << bname); if (type) *type = buildlog; return bname.absFileName(); // If we have a newer master file log or only a master log, show this } else if (fname != masterfname && (!fname.exists() && (masterfname.exists() || fname.lastModified() < masterfname.lastModified()))) { LYXERR(Debug::FILES, "Log name calculated as: " << masterfname); if (type) *type = mtype; return masterfname.absFileName(); } LYXERR(Debug::FILES, "Log name calculated as: " << fname); if (type) *type = latexlog; return fname.absFileName(); } void Buffer::setReadonly(bool const flag) { if (d->read_only != flag) { d->read_only = flag; changed(false); } } void Buffer::setFileName(FileName const & fname) { bool const changed = fname != d->filename; d->filename = fname; d->refreshFileMonitor(); if (changed) lyxvc().file_found_hook(fname); setReadonly(d->filename.isReadOnly()); saveCheckSum(); updateTitles(); } int Buffer::readHeader(Lexer & lex) { int unknown_tokens = 0; int line = -1; int begin_header_line = -1; // Initialize parameters that may be/go lacking in header: params().branchlist().clear(); params().preamble.erase(); params().options.erase(); params().master.erase(); params().float_placement.erase(); params().float_alignment.erase(); params().paperwidth.erase(); params().paperheight.erase(); params().leftmargin.erase(); params().rightmargin.erase(); params().topmargin.erase(); params().bottommargin.erase(); params().headheight.erase(); params().headsep.erase(); params().footskip.erase(); params().columnsep.erase(); params().font_roman_opts.erase(); params().font_sans_opts.erase(); params().font_typewriter_opts.erase(); params().fonts_cjk.erase(); params().listings_params.clear(); params().clearLayoutModules(); params().clearRemovedModules(); params().clearIncludedChildren(); params().pdfoptions().clear(); params().document_metadata.clear(); params().indiceslist().clear(); params().backgroundcolor = lyx::rgbFromHexName("#ffffff"); params().isbackgroundcolor = false; params().fontcolor = RGBColor(0, 0, 0); params().isfontcolor = false; params().notefontcolor = RGBColor(0xCC, 0xCC, 0xCC); params().isnotefontcolor = false; params().boxbgcolor = RGBColor(0xFF, 0, 0); params().isboxbgcolor = false; params().html_latex_start.clear(); params().html_latex_end.clear(); params().html_math_img_scale = 1.0; params().output_sync_macro.erase(); params().setLocalLayout(docstring(), false); params().setLocalLayout(docstring(), true); params().biblio_opts.erase(); params().biblatex_bibstyle.erase(); params().biblatex_citestyle.erase(); params().multibib.erase(); params().lineno_opts.clear(); params().spellignore().clear(); for (int i = 0; i < 4; ++i) { params().user_defined_bullet(i) = ITEMIZE_DEFAULTS[i]; params().temp_bullet(i) = ITEMIZE_DEFAULTS[i]; } ErrorList & errorList = d->errorLists["Parse"]; while (lex.isOK()) { string token; lex >> token; if (token.empty()) continue; if (token == "\\end_header") break; ++line; if (token == "\\begin_header") { begin_header_line = line; continue; } LYXERR(Debug::PARSER, "Handling document header token: `" << token << '\''); string const result = params().readToken(lex, token, d->filename); if (!result.empty()) { if (token == "\\textclass") { d->layout_position = result; } else { ++unknown_tokens; docstring const s = bformat(_("Unknown token: " "%1$s %2$s\n"), from_utf8(token), lex.getDocString()); errorList.push_back(ErrorItem(_("Document header error"), s)); } } } if (begin_header_line) { docstring const s = _("\\begin_header is missing"); errorList.push_back(ErrorItem(_("Document header error"), s)); } params().shell_escape = theSession().shellescapeFiles().find(absFileName()); params().makeDocumentClass(isClone(), isInternal()); return unknown_tokens; } // Uwe C. Schroeder // changed to be public and have one parameter // Returns true if "\end_document" is not read (Asger) bool Buffer::readDocument(Lexer & lex) { ErrorList & errorList = d->errorLists["Parse"]; errorList.clear(); // remove dummy empty par paragraphs().clear(); if (!lex.checkFor("\\begin_document")) { docstring const s = _("\\begin_document is missing"); errorList.push_back(ErrorItem(_("Document header error"), s)); } readHeader(lex); if (params().output_changes) { bool xcolorulem = LaTeXFeatures::isAvailable("ulem") && LaTeXFeatures::isAvailable("xcolor"); if (!xcolorulem) { Alert::warning(_("Changes not shown in LaTeX output"), _("Changes will not be highlighted in LaTeX output, " "because xcolor and ulem are not installed.\n" "Please install both packages or redefine " "\\lyxadded and \\lyxdeleted in the LaTeX preamble.")); } } if (!parent() && !params().master.empty()) { FileName const master_file = makeAbsPath(params().master, onlyPath(absFileName())); if (isLyXFileName(master_file.absFileName())) { Buffer * master = checkAndLoadLyXFile(master_file, true); if (master) { // necessary e.g. after a reload // to re-register the child (bug 5873) // FIXME: clean up updateMacros (here, only // child registering is needed). master->updateMacros(); // set master as master buffer, but only // if we are a real child if (master->isChild(this)) setParent(master); // if the master is not fully loaded // it is probably just loading this // child. No warning needed then. else if (master->isFullyLoaded()) LYXERR0("The master '" << params().master << "' assigned to this document (" << absFileName() << ") does not include " "this document. Ignoring the master assignment."); // If the master has just been created, un-hide it (#11162) if (!master->fileName().exists()) lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH, master->absFileName())); } } } // assure we have a default index params().indiceslist().addDefault(B_("Index")); // read main text if (FileName::isAbsolute(params().origin)) d->old_position = params().origin; else d->old_position = filePath(); if (!parent()) clearIncludeList(); bool const res = text().read(lex, errorList, d->inset); d->old_position.clear(); // inform parent buffer about local macros if (parent()) { Buffer const * pbuf = parent(); for (auto const & m : usermacros) pbuf->usermacros.insert(m); } usermacros.clear(); updateMacros(); updateMacroInstances(InternalUpdate); return res; } bool Buffer::isSyncTeXenabled() const { bool enabled = params().output_sync; if (!enabled) for (auto const & c : theConverters()) { const string dest = c.to().substr(0,3); if (dest == "dvi" || dest == "pdf") { const string cmd = c.command(); enabled |= cmd.find("-synctex=") != string::npos && cmd.find("-synctex=0") == string::npos; if (enabled) break; } } return enabled; } bool Buffer::importString(string const & format, docstring const & contents, ErrorList & errorList) { Format const * fmt = theFormats().getFormat(format); if (!fmt) return false; // It is important to use the correct extension here, since some // converters create a wrong output file otherwise (e.g. html2latex) FileName const name = tempFileName("Buffer_importStringXXXXXX." + fmt->extension()); ofdocstream os(name.toFilesystemEncoding().c_str()); // Do not convert os implicitly to bool, since that is forbidden in C++11. bool const success = !(os << contents).fail(); os.close(); bool converted = false; if (success) { params().compressed = false; // remove dummy empty par paragraphs().clear(); converted = importFile(format, name, errorList); } removeTempFile(name); return converted; } bool Buffer::importFile(string const & format, FileName const & name, ErrorList & errorList) { if (!theConverters().isReachable(format, "lyx")) return false; FileName const lyx = tempFileName("Buffer_importFileXXXXXX.lyx"); Converters::RetVal const retval = theConverters().convert(this, name, lyx, name, format, "lyx", errorList); if (retval == Converters::SUCCESS) { bool const success = readFile(lyx) == ReadSuccess; removeTempFile(lyx); return success; } return false; } bool Buffer::readString(string const & s) { params().compressed = false; Lexer lex; istringstream is(s); lex.setStream(is); TempFile tempfile("Buffer_readStringXXXXXX.lyx"); FileName const fn = tempfile.name(); int file_format; bool success = parseLyXFormat(lex, fn, file_format) == ReadSuccess; if (success && file_format != LYX_FORMAT) { // We need to call lyx2lyx, so write the input to a file ofstream os(fn.toFilesystemEncoding().c_str()); os << s; os.close(); // lyxvc in readFile if (readFile(fn) != ReadSuccess) success = false; } else if (success) if (readDocument(lex)) success = false; return success; } Buffer::ReadStatus Buffer::readFile(FileName const & fn) { Lexer lex; if (!lex.setFile(fn)) { Alert::error(_("File Not Found"), bformat(_("Unable to open file `%1$s'."), from_utf8(fn.absFileName()))); return ReadFileNotFound; } int file_format; ReadStatus const ret_plf = parseLyXFormat(lex, fn, file_format); if (ret_plf != ReadSuccess) return ret_plf; if (file_format != LYX_FORMAT) { FileName tmpFile; ReadStatus ret_clf = convertLyXFormat(fn, tmpFile, file_format); if (ret_clf != ReadSuccess) return ret_clf; ret_clf = readFile(tmpFile); if (ret_clf == ReadSuccess) { d->file_format = file_format; d->need_format_backup = true; } return ret_clf; } // FIXME: InsetInfo needs to know whether the file is under VCS // during the parse process, so this has to be done before. lyxvc().file_found_hook(d->filename); if (readDocument(lex)) { Alert::error(_("Document format failure"), bformat(_("%1$s ended unexpectedly, which means" " that it is probably corrupted."), from_utf8(fn.absFileName()))); return ReadDocumentFailure; } d->file_fully_loaded = true; d->read_only = !d->filename.isWritable(); params().compressed = theFormats().isZippedFile(d->filename); saveCheckSum(); return ReadSuccess; } bool Buffer::isFullyLoaded() const { return d->file_fully_loaded; } void Buffer::setFullyLoaded(bool value) { d->file_fully_loaded = value; } bool Buffer::freshStartRequired() const { return d->require_fresh_start_; } void Buffer::requireFreshStart(bool const b) const { d->require_fresh_start_ = b; } PreviewLoader * Buffer::loader() const { if (!isExporting() && lyxrc.preview == LyXRC::PREVIEW_OFF) return nullptr; if (!d->preview_loader_) d->preview_loader_ = new PreviewLoader(*this); return d->preview_loader_; } void Buffer::removePreviews() const { delete d->preview_loader_; d->preview_loader_ = nullptr; } void Buffer::updatePreviews() const { PreviewLoader * ploader = loader(); if (!ploader) return; InsetIterator it = begin(*d->inset); InsetIterator const itend = end(*d->inset); for (; it != itend; ++it) it->addPreview(it, *ploader); ploader->startLoading(); } Buffer::ReadStatus Buffer::parseLyXFormat(Lexer & lex, FileName const & fn, int & file_format) const { if(!lex.checkFor("\\lyxformat")) { Alert::error(_("Document format failure"), bformat(_("%1$s is not a readable LyX document."), from_utf8(fn.absFileName()))); return ReadNoLyXFormat; } string tmp_format; lex >> tmp_format; // LyX formats 217 and earlier were written as 2.17. This corresponds // to files from LyX versions < 1.1.6.3. We just remove the dot in // these cases. See also: www.lyx.org/trac/changeset/1313. size_t dot = tmp_format.find_first_of(".,"); if (dot != string::npos) tmp_format.erase(dot, 1); file_format = convert(tmp_format); return ReadSuccess; } Buffer::ReadStatus Buffer::convertLyXFormat(FileName const & fn, FileName & tmpfile, int from_format) { TempFile tempfile("Buffer_convertLyXFormatXXXXXX.lyx"); tempfile.setAutoRemove(false); tmpfile = tempfile.name(); if(tmpfile.empty()) { Alert::error(_("Conversion failed"), bformat(_("%1$s is from a different" " version of LyX, but a temporary" " file for converting it could" " not be created."), from_utf8(fn.absFileName()))); return LyX2LyXNoTempFile; } FileName const lyx2lyx = libFileSearch("lyx2lyx", "lyx2lyx"); if (lyx2lyx.empty()) { Alert::error(_("Conversion script not found"), bformat(_("%1$s is from a different" " version of LyX, but the" " conversion script lyx2lyx" " could not be found."), from_utf8(fn.absFileName()))); return LyX2LyXNotFound; } // Run lyx2lyx: // $python$ "$lyx2lyx$" -t $LYX_FORMAT$ -o "$tempfile$" "$filetoread$" ostringstream command; command << os::python() << ' ' << quoteName(lyx2lyx.toFilesystemEncoding()) << " -t " << convert(LYX_FORMAT) << " -o " << quoteName(tmpfile.toSafeFilesystemEncoding()) << ' ' << quoteName(fn.toSafeFilesystemEncoding()); string const command_str = command.str(); LYXERR(Debug::INFO, "Running '" << command_str << '\''); cmd_ret const ret = runCommand(command_str); if (!ret.valid) { if (from_format < LYX_FORMAT) { Alert::error(_("Conversion script failed"), bformat(_("%1$s is from an older version" " of LyX and the lyx2lyx script" " failed to convert it."), from_utf8(fn.absFileName()))); return LyX2LyXOlderFormat; } else { Alert::error(_("Conversion script failed"), bformat(_("%1$s is from a newer version" " of LyX and the lyx2lyx script" " failed to convert it."), from_utf8(fn.absFileName()))); return LyX2LyXNewerFormat; } } return ReadSuccess; } FileName Buffer::getBackupName() const { map const file_formats = { {544, "23"}, {508, "22"}, {474, "21"}, {413, "20"}, {345, "16"}, {276, "15"}, {245, "14"}, {221, "13"}, {220, "12"}, {218, "1163"}, {217, "116"}, {216, "115"}, {215, "11"}, {210, "010"}, {200, "006"} }; FileName const & fn = fileName(); string const fname = fn.onlyFileNameWithoutExt(); string const fext = fn.extension() + "~"; string const fpath = lyxrc.backupdir_path.empty() ? fn.onlyPath().absFileName() : lyxrc.backupdir_path; string backup_suffix; // If file format is from a stable series use version instead of file format auto const it = file_formats.find(d->file_format); if (it != file_formats.end()) backup_suffix = "-lyx" + it->second; else backup_suffix = "-lyxformat-" + convert(d->file_format); string const backname = fname + backup_suffix; FileName backup(addName(fpath, addExtension(backname, fext))); // limit recursion, just in case int v = 1; unsigned long orig_checksum = 0; while (backup.exists() && v < 100) { if (orig_checksum == 0) orig_checksum = fn.checksum(); unsigned long new_checksum = backup.checksum(); if (orig_checksum == new_checksum) { LYXERR(Debug::FILES, "Not backing up " << fn << "since " << backup << "has the same checksum."); // a bit of a hack, but we have to check this anyway // below, and setting this is simpler than introducing // a special boolean for this purpose. v = 1000; break; } string const newbackname = backname + "-" + convert(v); backup.set(addName(fpath, addExtension(newbackname, fext))); v++; } return v < 100 ? backup : FileName(); } // Should probably be moved to somewhere else: BufferView? GuiView? bool Buffer::save() const { docstring const file = makeDisplayPath(absFileName(), 20); d->filename.refresh(); // check the read-only status before moving the file as a backup if (d->filename.exists()) { bool const read_only = !d->filename.isWritable(); if (read_only) { Alert::warning(_("File is read-only"), bformat(_("The file %1$s cannot be written because it " "is marked as read-only."), file)); return false; } } // ask if the disk file has been externally modified (use checksum method) if (fileName().exists() && isChecksumModified()) { docstring text = bformat(_("Document %1$s has been externally modified. " "Are you sure you want to overwrite this file?"), file); int const ret = Alert::prompt(_("Overwrite modified file?"), text, 1, 1, _("&Overwrite"), _("&Cancel")); if (ret == 1) return false; } // We don't need autosaves in the immediate future. (Asger) resetAutosaveTimers(); // if the file does not yet exist, none of the backup activity // that follows is necessary if (!fileName().exists()) { if (!writeFile(fileName())) return false; markClean(); return true; } // we first write the file to a new name, then move it to its // proper location once that has been done successfully. that // way we preserve the original file if something goes wrong. string const justname = fileName().onlyFileNameWithoutExt(); auto tempfile = lyx::make_unique(fileName().onlyPath(), justname + "-XXXXXX.lyx"); bool const symlink = fileName().isSymLink(); if (!symlink) tempfile->setAutoRemove(false); FileName savefile(tempfile->name()); LYXERR(Debug::FILES, "Saving to " << savefile.absFileName()); if (!savefile.clonePermissions(fileName())) LYXERR0("Failed to clone the permission from " << fileName().absFileName() << " to " << savefile.absFileName()); if (!writeFile(savefile)) return false; // we will set this to false if we fail bool made_backup = true; FileName backupName; bool const needBackup = lyxrc.make_backup || d->need_format_backup; if (needBackup) { if (d->need_format_backup) backupName = getBackupName(); // If we for some reason failed to find a backup name in case of // a format change, this will still set one. It's the best we can // do in this case. if (backupName.empty()) { backupName.set(fileName().absFileName() + "~"); if (!lyxrc.backupdir_path.empty()) { string const mangledName = subst(subst(backupName.absFileName(), '/', '!'), ':', '!'); backupName.set(addName(lyxrc.backupdir_path, mangledName)); } } LYXERR(Debug::FILES, "Backing up original file to " << backupName.absFileName()); // Except file is symlink do not copy because of #6587. // Hard links have bad luck. made_backup = symlink ? fileName().copyTo(backupName): fileName().moveTo(backupName); if (!made_backup) { Alert::error(_("Backup failure"), bformat(_("Cannot create backup file %1$s.\n" "Please check whether the directory exists and is writable."), from_utf8(backupName.absFileName()))); //LYXERR(Debug::DEBUG, "Fs error: " << fe.what()); } else if (d->need_format_backup) { // the original file has been backed up successfully, so we // will not need to do that again d->need_format_backup = false; } } // Destroy tempfile since it keeps the file locked on windows (bug 9234) // Only do this if tempfile is not in autoremove mode if (!symlink) tempfile.reset(); // If we have no symlink, we can simply rename the temp file. // Otherwise, we need to copy it so the symlink stays intact. if (made_backup && symlink ? savefile.copyTo(fileName(), true) : savefile.moveTo(fileName())) { // saveCheckSum() was already called by writeFile(), but the // time stamp is invalidated by copying/moving saveCheckSum(); markClean(); // the file associated with this buffer is now in the current format d->file_format = LYX_FORMAT; return true; } // else we saved the file, but failed to move it to the right location. if (needBackup && made_backup && !symlink) { // the original file was moved to some new location, so it will look // to the user as if it was deleted. (see bug #9234.) we could try // to restore it, but that would basically mean trying to do again // what we just failed to do. better to leave things as they are. Alert::error(_("Write failure"), bformat(_("The file has successfully been saved as:\n %1$s.\n" "But LyX could not move it to:\n %2$s.\n" "Your original file has been backed up to:\n %3$s"), from_utf8(savefile.absFileName()), from_utf8(fileName().absFileName()), from_utf8(backupName.absFileName()))); } else { // either we did not try to make a backup, or else we tried and failed, // or else the original file was a symlink, in which case it was copied, // not moved. so the original file is intact. Alert::error(_("Write failure"), bformat(_("Cannot move saved file to:\n %1$s.\n" "But the file has successfully been saved as:\n %2$s."), from_utf8(fileName().absFileName()), from_utf8(savefile.absFileName()))); } return false; } bool Buffer::writeFile(FileName const & fname) const { if (d->read_only && fname == d->filename) return false; bool retval = false; docstring const str = bformat(_("Saving document %1$s..."), makeDisplayPath(fname.absFileName())); message(str); string const encoded_fname = fname.toSafeFilesystemEncoding(os::CREATE); if (params().compressed) { gz::ogzstream ofs(encoded_fname.c_str(), ios::out|ios::trunc); retval = ofs && write(ofs); } else { ofstream ofs(encoded_fname.c_str(), ios::out|ios::trunc); retval = ofs && write(ofs); } if (!retval) { message(str + _(" could not write file!")); return false; } // see bug 6587 // removeAutosaveFile(); saveCheckSum(); message(str + _(" done.")); return true; } docstring Buffer::emergencyWrite() const { // No need to save if the buffer has not changed. if (isClean()) return docstring(); string const doc = isUnnamed() ? onlyFileName(absFileName()) : absFileName(); docstring user_message = bformat( _("LyX: Attempting to save document %1$s\n"), from_utf8(doc)); // We try to save three places: // 1) Same place as document. Unless it is an unnamed doc. if (!isUnnamed()) { string s = absFileName(); s += ".emergency"; LYXERR0(" " << s); if (writeFile(FileName(s))) { markClean(); user_message += " " + bformat(_("Saved to %1$s. Phew.\n"), from_utf8(s)); return user_message; } else { user_message += " " + _("Save failed! Trying again...\n"); } } // 2) In HOME directory. string s = addName(Package::get_home_dir().absFileName(), absFileName()); s += ".emergency"; lyxerr << ' ' << s << endl; if (writeFile(FileName(s))) { markClean(); user_message += " " + bformat(_("Saved to %1$s. Phew.\n"), from_utf8(s)); return user_message; } user_message += " " + _("Save failed! Trying yet again...\n"); // 3) In "/tmp" directory. // MakeAbsPath to prepend the current // drive letter on OS/2 s = addName(package().temp_dir().absFileName(), absFileName()); s += ".emergency"; lyxerr << ' ' << s << endl; if (writeFile(FileName(s))) { markClean(); user_message += " " + bformat(_("Saved to %1$s. Phew.\n"), from_utf8(s)); return user_message; } user_message += " " + _("Save failed! Document is lost."); // Don't try again. markClean(); return user_message; } bool Buffer::write(ostream & ofs) const { #ifdef HAVE_LOCALE // Use the standard "C" locale for file output. ofs.imbue(locale::classic()); #endif // The top of the file should not be written by params(). // write out a comment in the top of the file // Important: Keep the version formatting in sync with lyx2lyx and // tex2lyx (bug 7951) ofs << "#LyX " << lyx_version_major << "." << lyx_version_minor << " created this file. For more info see https://www.lyx.org/\n" << "\\lyxformat " << LYX_FORMAT << "\n" << "\\begin_document\n"; /// For each author, set 'used' to true if there is a change /// by this author in the document; otherwise set it to 'false'. for (Author const & a : params().authors()) a.setUsed(false); ParIterator const end = const_cast(this)->par_iterator_end(); ParIterator it = const_cast(this)->par_iterator_begin(); for ( ; it != end; ++it) it->checkAuthors(params().authors()); // now write out the buffer parameters. ofs << "\\begin_header\n"; params().writeFile(ofs, this); ofs << "\\end_header\n"; // write the text ofs << "\n\\begin_body\n"; text().write(ofs); ofs << "\n\\end_body\n"; // Write marker that shows file is complete ofs << "\\end_document" << endl; // Shouldn't really be needed.... //ofs.close(); // how to check if close went ok? // Following is an attempt... (BE 20001011) // good() returns false if any error occurred, including some // formatting error. // bad() returns true if something bad happened in the buffer, // which should include file system full errors. bool status = true; if (!ofs) { status = false; lyxerr << "File was not closed properly." << endl; } return status; } Buffer::ExportStatus Buffer::makeLaTeXFile(FileName const & fname, string const & original_path, OutputParams const & runparams_in, OutputWhat output) const { OutputParams runparams = runparams_in; string const encoding = runparams.encoding->iconvName(); LYXERR(Debug::OUTFILE, "makeLaTeXFile encoding: " << encoding << ", fname=" << fname.realPath()); ofdocstream ofs; try { ofs.reset(encoding); } catch (iconv_codecvt_facet_exception const & e) { lyxerr << "Caught iconv exception: " << e.what() << endl; Alert::error(_("Iconv software exception Detected"), bformat(_("Please verify that the `iconv' support software is" " properly installed and supports the selected encoding" " (%1$s), or change the encoding in" " Document>Settings>Language."), from_ascii(encoding))); return ExportError; } if (!openFileWrite(ofs, fname)) return ExportError; ErrorList & errorList = d->errorLists["Export"]; errorList.clear(); ExportStatus status = ExportSuccess; otexstream os(ofs); // make sure we are ready to export // this needs to be done before we validate // FIXME Do we need to do this all the time? I.e., in children // of a master we are exporting? updateBuffer(); updateMacroInstances(OutputUpdate); ExportStatus retval; try { retval = writeLaTeXSource(os, original_path, runparams, output); if (retval == ExportKilled) return ExportKilled; } catch (EncodingException const & e) { docstring const failed(1, e.failed_char); ostringstream oss; oss << "0x" << hex << static_cast(e.failed_char) << dec; if (getParFromID(e.par_id).paragraph().layout().pass_thru) { docstring msg = bformat(_("Uncodable character '%1$s'" " (code point %2$s)"), failed, from_utf8(oss.str())); errorList.push_back(ErrorItem(msg, _("Some characters of your document are not " "representable in specific verbatim contexts.\n" "Changing the document encoding to utf8 could help."), {e.par_id, e.pos}, {e.par_id, e.pos + 1})); } else { docstring msg = bformat(_("Could not find LaTeX command for character '%1$s'" " (code point %2$s)"), failed, from_utf8(oss.str())); errorList.push_back(ErrorItem(msg, _("Some characters of your document are probably not " "representable in the chosen encoding.\n" "Changing the document encoding to utf8 could help."), {e.par_id, e.pos}, {e.par_id, e.pos + 1})); } status = ExportError; } catch (iconv_codecvt_facet_exception const & e) { errorList.push_back(ErrorItem(_("iconv conversion failed"), _(e.what()))); status = ExportError; } catch (exception const & e) { errorList.push_back(ErrorItem(_("conversion failed"), _(e.what()))); lyxerr << e.what() << endl; status = ExportError; } catch (...) { lyxerr << "Caught some really weird exception..." << endl; lyx_exit(1); } d->texrow = std::move(os.texrow()); ofs.close(); if (ofs.fail()) { status = ExportError; lyxerr << "File '" << fname << "' was not closed properly." << endl; } if (runparams_in.silent) errorList.clear(); else errors("Export"); return status; } Buffer::ExportStatus Buffer::writeLaTeXSource(otexstream & os, string const & original_path, OutputParams const & runparams_in, OutputWhat output) const { // The child documents, if any, shall be already loaded at this point. OutputParams runparams = runparams_in; // Some macros rely on font encoding runparams.main_fontenc = params().main_font_encoding(); // If we are compiling a file standalone, even if this is the // child of some other buffer, let's cut the link here, so the // file is really independent and no concurring settings from // the master (e.g. branch state) interfere (see #8100). if (!runparams.is_child) d->ignore_parent = true; // Classify the unicode characters appearing in math insets BufferEncodings::initUnicodeMath(*this); // validate the buffer. LYXERR(Debug::OUTFILE, " Validating buffer..."); LaTeXFeatures features(*this, params(), runparams); validate(features); // This is only set once per document (in master) if (!runparams.is_child) { runparams.use_polyglossia = features.usePolyglossia(); runparams.use_hyperref = features.isRequired("hyperref"); runparams.use_CJK = features.mustProvide("CJK"); } LYXERR(Debug::OUTFILE, " Buffer validation done."); bool const output_preamble = output == FullSource || output == OnlyPreamble; bool const output_body = output == FullSource || output == OnlyBody; // The starting paragraph of the coming rows is the // first paragraph of the document. (Asger) if (output_preamble && runparams.nice) { os << "%% LyX " << lyx_version << " created this file. " "For more info, see https://www.lyx.org/.\n" "%% Do not edit unless you really know what " "you are doing.\n"; } LYXERR(Debug::INFO, "lyx document header finished"); // There are a few differences between nice LaTeX and usual files: // usual files have \batchmode and special input@path to allow // inclusion of figures specified by an explicitly relative path // (i.e., a path starting with './' or '../') with either \input or // \includegraphics, as the TEXINPUTS method doesn't work in this case. // input@path is set when the actual parameter original_path is set. // This is done for usual tex-file, but not for nice-latex-file. // (Matthias 250696) // Note that input@path is only needed for something the user does // in the preamble, included .tex files or ERT, files included by // LyX work without it. if (output_preamble) { if (!runparams.nice) { // code for usual, NOT nice-latex-file os << "\\batchmode\n"; // changed from \nonstopmode } if (!original_path.empty()) { // FIXME UNICODE // We don't know the encoding of inputpath docstring const inputpath = from_utf8(original_path); docstring uncodable_glyphs; Encoding const * const enc = runparams.encoding; if (enc) { for (char_type n : inputpath) { if (!enc->encodable(n)) { docstring const glyph(1, n); LYXERR0("Uncodable character '" << glyph << "' in input path!"); uncodable_glyphs += glyph; } } } // warn user if we found uncodable glyphs. if (!uncodable_glyphs.empty()) { frontend::Alert::warning( _("Uncodable character in file path"), bformat( _("The path of your document\n" "(%1$s)\n" "contains glyphs that are unknown " "in the current document encoding " "(namely %2$s). This may result in " "incomplete output, unless " "TEXINPUTS contains the document " "directory and you don't use " "explicitly relative paths (i.e., " "paths starting with './' or " "'../') in the preamble or in ERT." "\n\nIn case of problems, choose " "an appropriate document encoding\n" "(such as utf8) or change the " "file path name."), inputpath, uncodable_glyphs)); } else { string docdir = os::latex_path(original_path); if (contains(docdir, '#')) { docdir = subst(docdir, "#", "\\#"); os << "\\catcode`\\#=11" "\\def\\#{#}\\catcode`\\#=6\n"; } if (contains(docdir, '%')) { docdir = subst(docdir, "%", "\\%"); os << "\\catcode`\\%=11" "\\def\\%{%}\\catcode`\\%=14\n"; } if (contains(docdir, '~')) docdir = subst(docdir, "~", "\\string~"); bool const nonascii = !isAscii(from_utf8(docdir)); // LaTeX 2019/10/01 handles non-ascii path without detokenize bool const utfpathlatex = features.isAvailableAtLeastFrom("LaTeX", 2019, 10); bool const detokenize = !utfpathlatex && nonascii; bool const quote = contains(docdir, ' '); if (utfpathlatex && nonascii) os << "\\UseRawInputEncoding\n"; os << "\\makeatletter\n" << "\\def\\input@path{{"; if (detokenize) os << "\\detokenize{"; if (quote) os << "\""; os << docdir; if (quote) os << "\""; if (detokenize) os << "}"; os << "}}\n" << "\\makeatother\n"; } } // get parent macros (if this buffer has a parent) which will be // written at the document begin further down. MacroSet parentMacros; listParentMacros(parentMacros, features); // Write the preamble runparams.use_babel = params().writeLaTeX(os, features, d->filename.onlyPath()); // Active characters runparams.active_chars = features.getActiveChars(); // Biblatex bibliographies are loaded here if (params().useBiblatex()) { vector> const bibfiles = prepareBibFilePaths(runparams, getBibfiles(), true); for (pair const & file: bibfiles) { os << "\\addbibresource"; if (!file.second.empty()) os << "[bibencoding=" << file.second << "]"; os << "{" << file.first << "}\n"; } } if (!runparams.dryrun && features.hasPolyglossiaExclusiveLanguages() && !features.hasOnlyPolyglossiaLanguages()) { docstring blangs; docstring plangs; vector bll = features.getBabelExclusiveLanguages(); vector pll = features.getPolyglossiaExclusiveLanguages(); if (!bll.empty()) { docstring langs; for (string const & sit : bll) { if (!langs.empty()) langs += ", "; langs += _(sit); } blangs = bll.size() > 1 ? bformat(_("The languages %1$s are only supported by Babel."), langs) : bformat(_("The language %1$s is only supported by Babel."), langs); } if (!pll.empty()) { docstring langs; for (string const & pit : pll) { if (!langs.empty()) langs += ", "; langs += _(pit); } plangs = pll.size() > 1 ? bformat(_("The languages %1$s are only supported by Polyglossia."), langs) : bformat(_("The language %1$s is only supported by Polyglossia."), langs); if (!blangs.empty()) plangs += "\n"; } frontend::Alert::warning( _("Incompatible Languages!"), bformat( _("You cannot use the following languages " "together in one LaTeX document because " "they require conflicting language packages:\n" "%1$s%2$s"), plangs, blangs)); } // Japanese might be required only in some children of a document, // but once required, we must keep use_japanese true. runparams.use_japanese |= features.isRequired("japanese"); if (!output_body) { // Restore the parenthood if needed if (!runparams.is_child) d->ignore_parent = false; return ExportSuccess; } // make the body. // mark the beginning of the body to separate it from InPreamble insets os.texrow().start(TexRow::beginDocument()); os << "\\begin{document}\n"; // mark the start of a new paragraph by simulating a newline, // so that os.afterParbreak() returns true at document start os.lastChar('\n'); // output the parent macros for (auto const & mac : parentMacros) { int num_lines = mac->write(os.os(), true); os.texrow().newlines(num_lines); } } // output_preamble LYXERR(Debug::INFO, "preamble finished, now the body."); // the real stuff try { latexParagraphs(*this, text(), os, runparams); } catch (ConversionException const &) { return ExportKilled; } // Restore the parenthood if needed if (!runparams.is_child) d->ignore_parent = false; // add this just in case after all the paragraphs os << endl; if (output_preamble) { os << "\\end{document}\n"; LYXERR(Debug::OUTFILE, "makeLaTeXFile...done"); } else { LYXERR(Debug::OUTFILE, "LaTeXFile for inclusion made."); } runparams_in.encoding = runparams.encoding; LYXERR(Debug::INFO, "Finished making LaTeX file."); LYXERR(Debug::INFO, "Row count was " << os.texrow().rows() - 1 << '.'); return ExportSuccess; } Buffer::ExportStatus Buffer::makeDocBookFile(FileName const & fname, OutputParams const & runparams, OutputWhat output) const { LYXERR(Debug::OUTFILE, "makeDocBookFile..."); ofdocstream ofs; if (!openFileWrite(ofs, fname)) return ExportError; // make sure we are ready to export // this needs to be done before we validate updateBuffer(); updateMacroInstances(OutputUpdate); ExportStatus const retval = writeDocBookSource(ofs, runparams, output); if (retval == ExportKilled) return ExportKilled; ofs.close(); if (ofs.fail()) lyxerr << "File '" << fname << "' was not closed properly." << endl; return ExportSuccess; } Buffer::ExportStatus Buffer::writeDocBookSource(odocstream & os, OutputParams const & runparams, OutputWhat output) const { LaTeXFeatures features(*this, params(), runparams); validate(features); d->bibinfo_.makeCitationLabels(*this); d->texrow.reset(); DocumentClass const & tclass = params().documentClass(); bool const output_preamble = output == FullSource || output == OnlyPreamble; bool const output_body = output == FullSource || output == OnlyBody || output == IncludedFile; if (output_preamble) { // XML preamble, no doctype needed. // Not using XMLStream for this, as the root tag would be in the tag stack and make troubles with the error // detection mechanisms (these are called before the end tag is output, and thus interact with the canary // parsep in output_docbook.cpp). os << "\n" << "\n"; // Prepare the name space declaration for MathML depending on document preferences. string mathmlNamespace; if (params().docbook_mathml_prefix != BufferParams::NoPrefix) { string mathmlPrefix; if (params().docbook_mathml_prefix == BufferParams::MPrefix) mathmlPrefix = "m"; else if (params().docbook_mathml_prefix == BufferParams::MMLPrefix) mathmlPrefix = "mml"; mathmlNamespace = + " xmlns:" + mathmlPrefix + "=\"http://www.w3.org/1998/Math/MathML\""; } // XML-compatible language code: in lib/languages, language codes are // given as dictionary file names; HTML5 expects to follow BCP47. This // function implements a simple heuristic that does the conversion. std::string htmlCode = params().language->code(); std::replace(htmlCode.begin(), htmlCode.end(), '_', '-'); // Directly output the root tag, based on the current type of document. string attributes = "xml:lang=\"" + htmlCode + '"' + " xmlns=\"http://docbook.org/ns/docbook\"" + " xmlns:xlink=\"http://www.w3.org/1999/xlink\"" + mathmlNamespace + " xmlns:xi=\"http://www.w3.org/2001/XInclude\"" + " version=\"5.2\""; // Version 5.2 is required for formalgroup. os << "<" << from_ascii(tclass.docbookroot()) << " " << from_ascii(attributes) << ">\n"; } if (output_body) { // Start to output the document. XMLStream xs(os); docbookParagraphs(text(), *this, xs, runparams); } if (output_preamble) { // Close the root element. No need for a line break, as free text is never allowed // in a root element, it must always be wrapped in some container. os << ""; } return ExportSuccess; } Buffer::ExportStatus Buffer::makeLyXHTMLFile(FileName const & fname, OutputParams const & runparams) const { LYXERR(Debug::OUTFILE, "makeLyXHTMLFile..."); ofdocstream ofs; if (!openFileWrite(ofs, fname)) return ExportError; // make sure we are ready to export // this has to be done before we validate updateBuffer(UpdateMaster, OutputUpdate); updateMacroInstances(OutputUpdate); ExportStatus const retval = writeLyXHTMLSource(ofs, runparams, FullSource); if (retval == ExportKilled) return retval; ofs.close(); if (ofs.fail()) lyxerr << "File '" << fname << "' was not closed properly." << endl; return retval; } Buffer::ExportStatus Buffer::writeLyXHTMLSource(odocstream & os, OutputParams const & runparams, OutputWhat output) const { LaTeXFeatures features(*this, params(), runparams); validate(features); d->bibinfo_.makeCitationLabels(*this); bool const output_preamble = output == FullSource || output == OnlyPreamble; bool const output_body = output == FullSource || output == OnlyBody || output == IncludedFile; if (output_preamble) { // HTML5-compatible language code: in lib/languages, language codes are // given as dictionary file names; HTML5 expects to follow BCP47. This // function implements a simple heuristic that does the conversion. std::string htmlCode = params().language->code(); std::replace(htmlCode.begin(), htmlCode.end(), '_', '-'); os << "\n" << "\n" << "\n" << "\n"; docstring const & doctitle = features.htmlTitle(); os << "" << (doctitle.empty() ? from_ascii("LyX Document") : xml::escapeString(doctitle, XMLStream::ESCAPE_ALL)) << "\n"; docstring styles = features.getTClassHTMLPreamble(); if (!styles.empty()) os << "\n\n" << styles << '\n'; // we will collect CSS information in a stream, and then output it // either here, as part of the header, or else in a separate file. odocstringstream css; styles = features.getCSSSnippets(); if (!styles.empty()) css << "/* LyX Provided Styles */\n" << styles << '\n'; styles = features.getTClassHTMLStyles(); if (!styles.empty()) css << "/* Layout-provided Styles */\n" << styles << '\n'; bool const needfg = params().fontcolor != RGBColor(0, 0, 0); bool const needbg = params().backgroundcolor != RGBColor(0xFF, 0xFF, 0xFF); if (needfg || needbg) { css << "\nbody {\n"; if (needfg) css << " color: " << from_ascii(X11hexname(params().fontcolor)) << ";\n"; if (needbg) css << " background-color: " << from_ascii(X11hexname(params().backgroundcolor)) << ";\n"; css << "}\n"; } docstring const dstyles = css.str(); if (!dstyles.empty()) { bool written = false; if (params().html_css_as_file) { // open a file for CSS info ofdocstream ocss; string const fcssname = addName(temppath(), "docstyle.css"); FileName const fcssfile = FileName(fcssname); if (openFileWrite(ocss, fcssfile)) { ocss << dstyles; ocss.close(); written = true; // write link to header os << "\n"; // register file runparams.exportdata->addExternalFile("xhtml", fcssfile); } } // we are here if the CSS is supposed to be written to the header // or if we failed to write it to an external file. if (!written) { os << "\n"; } } os << "\n"; } if (output_body) { bool const output_body_tag = (output != IncludedFile); if (output_body_tag) os << "\n"; XMLStream xs(os); if (output != IncludedFile) // if we're an included file, the counters are in the master. params().documentClass().counters().reset(); try { xhtmlParagraphs(text(), *this, xs, runparams); } catch (ConversionException const &) { return ExportKilled; } if (output_body_tag) os << "\n"; } if (output_preamble) os << "\n"; return ExportSuccess; } // chktex should be run with these flags disabled: 3, 22, 25, 30, 38(?) // Other flags: -wall -v0 -x int Buffer::runChktex() { setBusy(true); // get LaTeX-Filename FileName const path(temppath()); string const name = addName(path.absFileName(), latexName()); string const org_path = filePath(); PathChanger p(path); // path to LaTeX file message(_("Running chktex...")); // Generate the LaTeX file if neccessary OutputParams runparams(¶ms().encoding()); runparams.flavor = Flavor::LaTeX; runparams.nice = false; runparams.linelen = lyxrc.plaintext_linelen; ExportStatus const retval = makeLaTeXFile(FileName(name), org_path, runparams); if (retval != ExportSuccess) { // error code on failure return -1; } TeXErrors terr; Chktex chktex(lyxrc.chktex_command, onlyFileName(name), filePath()); int const res = chktex.run(terr); // run chktex if (res == -1) { Alert::error(_("chktex failure"), _("Could not run chktex successfully.")); } else { ErrorList & errlist = d->errorLists["ChkTeX"]; errlist.clear(); bufferErrors(terr, errlist); } setBusy(false); if (runparams.silent) d->errorLists["ChkTeX"].clear(); else errors("ChkTeX"); return res; } void Buffer::validate(LaTeXFeatures & features) const { // Validate the buffer params, but not for included // files, since they also use the parent buffer's // params (# 5941) if (!features.runparams().is_child) params().validate(features); if (!parent()) clearIncludeList(); for (Paragraph const & p : paragraphs()) p.validate(features); if (lyxerr.debugging(Debug::OUTFILE)) { features.showStruct(); } } void Buffer::getLabelList(vector> & list) const { // If this is a child document, use the master's list instead. if (parent()) { masterBuffer()->getLabelList(list); return; } list.clear(); shared_ptr toc = d->toc_backend.toc("label"); for (auto const & tocit : *toc) { if (tocit.depth() == 0) { list.push_back(make_tuple(tocit.str(), tocit.asString(), tocit.prettyStr())); } } } void Buffer::invalidateBibinfoCache() const { d->bibinfo_cache_valid_ = false; d->cite_labels_valid_ = false; removeBiblioTempFiles(); // also invalidate the cache for the parent buffer Buffer const * const pbuf = d->parent(); if (pbuf) pbuf->invalidateBibinfoCache(); } docstring_list const & Buffer::getBibfiles(UpdateScope scope) const { // FIXME This is probably unnecessary, given where we call this. // If this is a child document, use the master instead. Buffer const * const pbuf = masterBuffer(); if (pbuf != this && scope != UpdateChildOnly) return pbuf->getBibfiles(); // In 2.3.x, we have: //if (!d->bibfile_cache_valid_) // this->updateBibfilesCache(scope); // I think that is a leftover, but there have been so many back- // and-forths with this, due to Windows issues, that I am not sure. return d->bibfiles_cache_; } BiblioInfo const & Buffer::masterBibInfo() const { Buffer const * const tmp = masterBuffer(); if (tmp != this) return tmp->masterBibInfo(); return d->bibinfo_; } BiblioInfo const & Buffer::bibInfo() const { return d->bibinfo_; } void Buffer::registerBibfiles(const docstring_list & bf) const { // We register the bib files in the master buffer, // if there is one, but also in every single buffer, // in case a child is compiled alone. Buffer const * const tmp = masterBuffer(); if (tmp != this) tmp->registerBibfiles(bf); for (auto const & p : bf) { docstring_list::const_iterator temp = find(d->bibfiles_cache_.begin(), d->bibfiles_cache_.end(), p); if (temp == d->bibfiles_cache_.end()) d->bibfiles_cache_.push_back(p); } } static map bibfileCache; FileName Buffer::getBibfilePath(docstring const & bibid) const { map::const_iterator it = bibfileCache.find(bibid); if (it != bibfileCache.end()) { // i.e., return bibfileCache[bibid]; return it->second; } LYXERR(Debug::FILES, "Reading file location for " << bibid); string const texfile = changeExtension(to_utf8(bibid), "bib"); // we need to check first if this file exists where it's said to be. // there's a weird bug that occurs otherwise: if the file is in the // Buffer's directory but has the same name as some file that would be // found by kpsewhich, then we find the latter, not the former. FileName const local_file = makeAbsPath(texfile, filePath()); FileName file = local_file; if (!file.exists()) { // there's no need now to check whether the file can be found // locally file = findtexfile(texfile, "bib", true); if (file.empty()) file = local_file; } LYXERR(Debug::FILES, "Found at: " << file); bibfileCache[bibid] = file; return bibfileCache[bibid]; } void Buffer::checkIfBibInfoCacheIsValid() const { // use the master's cache Buffer const * const tmp = masterBuffer(); if (tmp != this) { tmp->checkIfBibInfoCacheIsValid(); return; } // If we already know the cache is invalid, stop here. // This is important in the case when the bibliography // environment (rather than Bib[la]TeX) is used. // In that case, the timestamp check below gives no // sensible result. Rather than that, the cache will // be invalidated explicitly via invalidateBibInfoCache() // by the Bibitem inset. // Same applies for bib encoding changes, which trigger // invalidateBibInfoCache() by InsetBibtex. if (!d->bibinfo_cache_valid_) return; if (d->have_bibitems_) { // We have a bibliography environment. // Invalidate the bibinfo cache unconditionally. // Cite labels will get invalidated by the inset if needed. d->bibinfo_cache_valid_ = false; return; } // OK. This is with Bib(la)tex. We'll assume the cache // is valid and change this if we find changes in the bibs. d->bibinfo_cache_valid_ = true; d->cite_labels_valid_ = true; // compare the cached timestamps with the actual ones. docstring_list const & bibfiles_cache = getBibfiles(); for (auto const & bf : bibfiles_cache) { FileName const fn = getBibfilePath(bf); time_t lastw = fn.lastModified(); time_t prevw = d->bibfile_status_[fn]; if (lastw != prevw) { d->bibinfo_cache_valid_ = false; d->cite_labels_valid_ = false; d->bibfile_status_[fn] = lastw; } } } void Buffer::clearBibFileCache() const { bibfileCache.clear(); } void Buffer::reloadBibInfoCache(bool const force) const { // we should not need to do this for internal buffers if (isInternal()) return; // use the master's cache Buffer const * const tmp = masterBuffer(); if (tmp != this) { tmp->reloadBibInfoCache(force); return; } if (!force) { checkIfBibInfoCacheIsValid(); if (d->bibinfo_cache_valid_) return; } LYXERR(Debug::FILES, "Bibinfo cache was invalid."); // re-read file locations when this info changes // FIXME Is this sufficient? Or should we also force that // in some other cases? If so, then it is easy enough to // add the following line in some other places. clearBibFileCache(); d->bibinfo_.clear(); FileNameList checkedFiles; d->have_bibitems_ = false; collectBibKeys(checkedFiles); d->bibinfo_cache_valid_ = true; } void Buffer::collectBibKeys(FileNameList & checkedFiles) const { if (!parent()) clearIncludeList(); for (InsetIterator it = begin(inset()); it; ++it) { it->collectBibKeys(it, checkedFiles); if (it->lyxCode() == BIBITEM_CODE) { if (parent() != nullptr) parent()->d->have_bibitems_ = true; else d->have_bibitems_ = true; } } } void Buffer::addBiblioInfo(BiblioInfo const & bi_in) const { // We add the biblio info to the parent buffer, // if there is one, but also to this buffer, in case // it is compiled alone. BiblioInfo & our_bi = d->bibinfo_; our_bi.mergeBiblioInfo(bi_in); if (parent()) parent()->addBiblioInfo(bi_in); } void Buffer::addBibTeXInfo(docstring const & key, BibTeXInfo const & bin) const { // We add the bibtex info to the master buffer, // if there is one, but also to every single buffer, // in case a child is compiled alone. BiblioInfo & bi = d->bibinfo_; bi[key] = bin; if (parent() != nullptr) { BiblioInfo & masterbi = masterBuffer()->d->bibinfo_; masterbi[key] = bin; } } void Buffer::makeCitationLabels() const { Buffer const * const master = masterBuffer(); return d->bibinfo_.makeCitationLabels(*master); } void Buffer::invalidateCiteLabels() const { masterBuffer()->d->cite_labels_valid_ = false; } bool Buffer::citeLabelsValid() const { return masterBuffer()->d->cite_labels_valid_; } void Buffer::removeBiblioTempFiles() const { if (theApp()->isBufferBusy(this)) { removeBiblioTemps = true; return; } // We remove files that contain LaTeX commands specific to the // particular bibliographic style being used, in order to avoid // LaTeX errors when we switch style. FileName const aux_file(addName(temppath(), changeExtension(latexName(),".aux"))); FileName const bbl_file(addName(temppath(), changeExtension(latexName(),".bbl"))); LYXERR(Debug::FILES, "Removing the .aux file " << aux_file); aux_file.removeFile(); LYXERR(Debug::FILES, "Removing the .bbl file " << bbl_file); bbl_file.removeFile(); // Also for the parent buffer Buffer const * const pbuf = parent(); if (pbuf) pbuf->removeBiblioTempFiles(); removeBiblioTemps = false; } bool Buffer::isDepClean(string const & name) const { DepClean::const_iterator const it = d->dep_clean.find(name); if (it == d->dep_clean.end()) return true; return it->second; } void Buffer::markDepClean(string const & name) { d->dep_clean[name] = true; } bool Buffer::branchActivationEnabled(FuncCode act, docstring const & branch) const { bool const master = act == LFUN_BRANCH_MASTER_ACTIVATE || act == LFUN_BRANCH_MASTER_DEACTIVATE; bool const activate = act == LFUN_BRANCH_ACTIVATE || act == LFUN_BRANCH_MASTER_ACTIVATE; Buffer const * buf = master ? masterBuffer() : this; Branch const * our_branch = buf->params().branchlist().find(branch); // Can be disabled if // - this is a _MASTER_ command and there is no master // - the relevant buffer does not know the branch // - the branch is already in the desired state return ((!master || parent() != nullptr) && !branch.empty() && our_branch && our_branch->isSelected() != activate); } bool Buffer::getStatus(FuncRequest const & cmd, FuncStatus & flag) const { if (isInternal()) { // FIXME? if there is an Buffer LFUN that can be dispatched even // if internal, put a switch '(cmd.action)' here. return false; } bool enable = true; switch (cmd.action()) { case LFUN_BUFFER_TOGGLE_READ_ONLY: flag.setOnOff(hasReadonlyFlag()); break; // FIXME: There is need for a command-line import. //case LFUN_BUFFER_IMPORT: case LFUN_BUFFER_AUTO_SAVE: break; case LFUN_BUFFER_EXPORT_CUSTOM: // FIXME: Nothing to check here? break; case LFUN_BUFFER_EXPORT: { docstring const & arg = cmd.argument(); if (arg == "custom") { enable = true; break; } string format = (arg.empty() || arg == "default") ? params().getDefaultOutputFormat() : to_utf8(arg); size_t pos = format.find(' '); if (pos != string::npos) format = format.substr(0, pos); enable = params().isExportable(format, false); if (!enable) flag.message(bformat( _("Don't know how to export to format: %1$s"), arg)); break; } case LFUN_BUILD_PROGRAM: enable = params().isExportable("program", false); break; case LFUN_BRANCH_ACTIVATE: case LFUN_BRANCH_DEACTIVATE: case LFUN_BRANCH_MASTER_ACTIVATE: case LFUN_BRANCH_MASTER_DEACTIVATE: // Let a branch inset handle that if (cmd.argument().empty()) return false; flag.setEnabled(branchActivationEnabled(cmd.action(), cmd.argument())); break; case LFUN_BRANCH_ADD: case LFUN_BRANCHES_RENAME: // if no Buffer is present, then of course we won't be called! break; case LFUN_BUFFER_LANGUAGE: enable = !isReadonly(); break; case LFUN_BUFFER_VIEW_CACHE: (d->preview_file_).refresh(); enable = (d->preview_file_).exists() && !(d->preview_file_).isFileEmpty(); break; case LFUN_CHANGES_TRACK: flag.setEnabled(true); flag.setOnOff(params().track_changes); break; case LFUN_CHANGES_OUTPUT: flag.setEnabled(true); flag.setOnOff(params().output_changes); break; case LFUN_BUFFER_TOGGLE_COMPRESSION: flag.setOnOff(params().compressed); break; case LFUN_BUFFER_TOGGLE_OUTPUT_SYNC: flag.setOnOff(params().output_sync); break; case LFUN_BUFFER_ANONYMIZE: break; default: return false; } flag.setEnabled(enable); return true; } bool Buffer::branchActivationDispatch(FuncCode act, docstring const & branch) { bool const master = (act == LFUN_BRANCH_MASTER_ACTIVATE || act == LFUN_BRANCH_MASTER_DEACTIVATE); bool const activate = (act == LFUN_BRANCH_ACTIVATE || act == LFUN_BRANCH_MASTER_ACTIVATE); Buffer * buf = master ? const_cast(masterBuffer()) : this; Branch * our_branch = buf->params().branchlist().find(branch); // See comment in branchActivationStatus if ((master && parent() == nullptr) || !our_branch || our_branch->isSelected() == activate) return false; if (master && !buf->hasGuiDelegate() && (!theApp() || !theApp()->unhide(buf))) // at least issue a warning for now (ugly, but better than dataloss). frontend::Alert::warning(_("Branch state changes in master document"), lyx::support::bformat(_("The state of the branch '%1$s' " "was changed in the master file. " "Please make sure to save the master."), branch), true); UndoGroupHelper ugh(buf); buf->undo().recordUndoBufferParams(CursorData()); our_branch->setSelected(activate); // cur.forceBufferUpdate() is not enough) buf->updateBuffer(); // if branch exists in a descendant, update previews. // TODO: only needed if "Show preview" is enabled in the included inset. bool exists_in_desc = false; for (auto const & it : buf->getDescendants()) { if (it->params().branchlist().find(branch)) exists_in_desc = true; } if (exists_in_desc) { // TODO: ideally we would only update the previews of the // specific children that have this branch directly or // in one of their descendants buf->removePreviews(); buf->updatePreviews(); } return true; } void Buffer::dispatch(string const & command, DispatchResult & result) { return dispatch(lyxaction.lookupFunc(command), result); } // NOTE We can end up here even if we have no GUI, because we are called // by LyX::exec to handled command-line requests. So we may need to check // whether we have a GUI or not. The boolean use_gui holds this information. void Buffer::dispatch(FuncRequest const & func, DispatchResult & dr) { LYXERR(Debug::ACTION, "Buffer::dispatch: cmd: " << func); if (isInternal()) { // FIXME? if there is an Buffer LFUN that can be dispatched even // if internal, put a switch '(cmd.action())' here. dr.dispatched(false); return; } string const argument = to_utf8(func.argument()); // We'll set this back to false if need be. bool dispatched = true; // This handles undo groups automagically UndoGroupHelper ugh(this); switch (func.action()) { case LFUN_BUFFER_TOGGLE_READ_ONLY: if (lyxvc().inUse()) { string log = lyxvc().toggleReadOnly(); if (!log.empty()) dr.setMessage(log); } else setReadonly(!hasReadonlyFlag()); break; case LFUN_BUFFER_EXPORT: { string const format = (argument.empty() || argument == "default") ? params().getDefaultOutputFormat() : argument; ExportStatus const status = doExport(format, false); dr.setError(status != ExportSuccess); if (status != ExportSuccess) dr.setMessage(bformat(_("Error exporting to format: %1$s."), from_utf8(format))); break; } case LFUN_BUILD_PROGRAM: { ExportStatus const status = doExport("program", true); dr.setError(status != ExportSuccess); if (status != ExportSuccess) dr.setMessage(_("Error generating literate programming code.")); break; } case LFUN_BUFFER_EXPORT_CUSTOM: { string format_name; string command = split(argument, format_name, ' '); Format const * format = theFormats().getFormat(format_name); if (!format) { lyxerr << "Format \"" << format_name << "\" not recognized!" << endl; break; } // The name of the file created by the conversion process string filename; // Output to filename if (format->name() == "lyx") { string const latexname = latexName(false); filename = changeExtension(latexname, format->extension()); filename = addName(temppath(), filename); if (!writeFile(FileName(filename))) break; } else { doExport(format_name, true, filename); } // Substitute $$FName for filename if (!contains(command, "$$FName")) command = "( " + command + " ) < $$FName"; command = subst(command, "$$FName", filename); // Execute the command in the background Systemcall call; call.startscript(Systemcall::DontWait, command, filePath(), layoutPos()); break; } // FIXME: There is need for a command-line import. /* case LFUN_BUFFER_IMPORT: doImport(argument); break; */ case LFUN_BUFFER_AUTO_SAVE: autoSave(); resetAutosaveTimers(); break; case LFUN_BRANCH_ACTIVATE: case LFUN_BRANCH_DEACTIVATE: case LFUN_BRANCH_MASTER_ACTIVATE: case LFUN_BRANCH_MASTER_DEACTIVATE: { // Let a branch inset handle that if (func.argument().empty()) { dr.dispatched(false); return; } bool const res = branchActivationDispatch(func.action(), func.argument()); dr.setError(!res); if (res) dr.screenUpdate(Update::Force); break; } case LFUN_BRANCH_ADD: { docstring const & branchnames = func.argument(); if (branchnames.empty()) { dispatched = false; break; } BranchList & branch_list = params().branchlist(); vector const branches = getVectorFromString(branchnames, branch_list.separator()); docstring msg; for (docstring const & branch_name : branches) { Branch * branch = branch_list.find(branch_name); if (branch) { LYXERR0("Branch " << branch_name << " already exists."); dr.setError(true); if (!msg.empty()) msg += ("\n"); msg += bformat(_("Branch \"%1$s\" already exists."), branch_name); } else { undo().recordUndoBufferParams(CursorData()); branch_list.add(branch_name); branch = branch_list.find(branch_name); if (branch) // needed to update the color table for dark mode branch->setColors("background", "background"); dr.setError(false); dr.screenUpdate(Update::Force); } } if (!msg.empty()) dr.setMessage(msg); break; } case LFUN_BRANCHES_RENAME: { if (func.argument().empty()) break; docstring const oldname = from_utf8(func.getArg(0)); docstring const newname = from_utf8(func.getArg(1)); InsetIterator it = begin(inset()); InsetIterator const itend = end(inset()); bool success = false; for (; it != itend; ++it) { if (it->lyxCode() == BRANCH_CODE) { InsetBranch & ins = static_cast(*it); if (ins.branch() == oldname) { undo().recordUndo(CursorData(it)); ins.rename(newname); success = true; continue; } } if (it->lyxCode() == INCLUDE_CODE) { // get buffer of external file InsetInclude const & ins = static_cast(*it); Buffer * child = ins.loadIfNeeded(); if (!child) continue; child->dispatch(func, dr); } } if (success) { dr.screenUpdate(Update::Force); dr.forceBufferUpdate(); } break; } case LFUN_BUFFER_VIEW_CACHE: if (!theFormats().view(*this, d->preview_file_, d->preview_format_)) dr.setMessage(_("Error viewing the output file.")); break; case LFUN_CHANGES_TRACK: if (params().save_transient_properties) undo().recordUndoBufferParams(CursorData()); params().track_changes = !params().track_changes; break; case LFUN_CHANGES_OUTPUT: if (params().save_transient_properties) undo().recordUndoBufferParams(CursorData()); params().output_changes = !params().output_changes; if (params().output_changes) { bool xcolorulem = LaTeXFeatures::isAvailable("ulem") && LaTeXFeatures::isAvailable("xcolor"); if (!xcolorulem) { Alert::warning(_("Changes not shown in LaTeX output"), _("Changes will not be highlighted in LaTeX output, " "because xcolor and ulem are not installed.\n" "Please install both packages or redefine " "\\lyxadded and \\lyxdeleted in the LaTeX preamble.")); } } break; case LFUN_BUFFER_TOGGLE_COMPRESSION: // turn compression on/off undo().recordUndoBufferParams(CursorData()); params().compressed = !params().compressed; break; case LFUN_BUFFER_TOGGLE_OUTPUT_SYNC: undo().recordUndoBufferParams(CursorData()); params().output_sync = !params().output_sync; break; case LFUN_BUFFER_ANONYMIZE: { undo().recordUndoFullBuffer(CursorData()); CursorData cur(doc_iterator_begin(this)); for ( ; cur ; cur.forwardPar()) cur.paragraph().anonymize(); dr.forceBufferUpdate(); dr.screenUpdate(Update::Force); break; } default: dispatched = false; break; } dr.dispatched(dispatched); } void Buffer::changeLanguage(Language const * from, Language const * to) { LASSERT(from, return); LASSERT(to, return); ParIterator it = par_iterator_begin(); ParIterator eit = par_iterator_end(); for (; it != eit; ++it) it->changeLanguage(params(), from, to); } bool Buffer::isMultiLingual() const { ParConstIterator end = par_iterator_end(); for (ParConstIterator it = par_iterator_begin(); it != end; ++it) if (it->isMultiLingual(params())) return true; return false; } std::set Buffer::getLanguages() const { std::set langs; getLanguages(langs); return langs; } void Buffer::getLanguages(std::set & langs) const { ParConstIterator end = par_iterator_end(); // add the buffer language, even if it's not actively used langs.insert(language()); // iterate over the paragraphs for (ParConstIterator it = par_iterator_begin(); it != end; ++it) it->getLanguages(langs); // also children ListOfBuffers clist = getDescendants(); for (auto const & cit : clist) cit->getLanguages(langs); } DocIterator Buffer::getParFromID(int const id) const { Buffer * buf = const_cast(this); if (id < 0) // This means non-existent return doc_iterator_end(buf); for (DocIterator it = doc_iterator_begin(buf); !it.atEnd(); it.forwardPar()) if (it.paragraph().id() == id) return it; return doc_iterator_end(buf); } bool Buffer::hasParWithID(int const id) const { return !getParFromID(id).atEnd(); } ParIterator Buffer::par_iterator_begin() { return ParIterator(doc_iterator_begin(this)); } ParIterator Buffer::par_iterator_end() { return ParIterator(doc_iterator_end(this)); } ParConstIterator Buffer::par_iterator_begin() const { return ParConstIterator(doc_iterator_begin(this)); } ParConstIterator Buffer::par_iterator_end() const { return ParConstIterator(doc_iterator_end(this)); } bool Buffer::empty() const { return paragraphs().size() == 1 && paragraphs().front().empty(); } Language const * Buffer::language() const { return params().language; } docstring Buffer::B_(string const & l10n) const { return params().B_(l10n); } bool Buffer::isClean() const { return d->lyx_clean; } bool Buffer::isChecksumModified() const { LASSERT(d->filename.exists(), return false); return d->checksum_ != d->filename.checksum(); } void Buffer::saveCheckSum() const { FileName const & file = d->filename; file.refresh(); d->checksum_ = file.exists() ? file.checksum() : 0; // in the case of save to a new file. } void Buffer::markClean() const { if (!d->lyx_clean) { d->lyx_clean = true; updateTitles(); } // if the .lyx file has been saved, we don't need an // autosave d->bak_clean = true; d->undo_.markDirty(); clearExternalModification(); } void Buffer::setUnnamed(bool flag) { d->unnamed = flag; } bool Buffer::isUnnamed() const { return d->unnamed; } /// \note /// Don't check unnamed, here: isInternal() is used in /// newBuffer(), where the unnamed flag has not been set by anyone /// yet. Also, for an internal buffer, there should be no need for /// retrieving fileName() nor for checking if it is unnamed or not. bool Buffer::isInternal() const { return d->internal_buffer; } void Buffer::setInternal(bool flag) { d->internal_buffer = flag; } void Buffer::markDirty() { if (d->lyx_clean) { d->lyx_clean = false; updateTitles(); } d->bak_clean = false; for (auto & depit : d->dep_clean) depit.second = false; } FileName Buffer::fileName() const { return d->filename; } string Buffer::absFileName() const { return d->filename.absFileName(); } string Buffer::filePath() const { string const abs = d->filename.onlyPath().absFileName(); if (abs.empty()) return abs; int last = abs.length() - 1; return abs[last] == '/' ? abs : abs + '/'; } DocFileName Buffer::getReferencedFileName(string const & fn) const { DocFileName result; if (FileName::isAbsolute(fn) || !FileName::isAbsolute(params().origin)) result.set(fn, filePath()); else { // filePath() ends with a path separator FileName const test(filePath() + fn); if (test.exists()) result.set(fn, filePath()); else result.set(fn, params().origin); } return result; } string const Buffer::prepareFileNameForLaTeX(string const & name, string const & ext, bool nice) const { string const fname = makeAbsPath(name, filePath()).absFileName(); if (FileName::isAbsolute(name) || !FileName(fname + ext).isReadableFile()) return name; if (!nice) return fname; // FIXME UNICODE return to_utf8(makeRelPath(from_utf8(fname), from_utf8(masterBuffer()->filePath()))); } vector> const Buffer::prepareBibFilePaths(OutputParams const & runparams, docstring_list const & bibfilelist, bool const add_extension) const { // If we are processing the LaTeX file in a temp directory then // copy the .bib databases to this temp directory, mangling their // names in the process. Store this mangled name in the list of // all databases. // (We need to do all this because BibTeX *really*, *really* // can't handle "files with spaces" and Windows users tend to // use such filenames.) // Otherwise, store the (maybe absolute) path to the original, // unmangled database name. vector> res; // determine the export format string const tex_format = flavor2format(runparams.flavor); // check for spaces in paths bool found_space = false; for (auto const & bit : bibfilelist) { string utf8input = to_utf8(bit); string database = prepareFileNameForLaTeX(utf8input, ".bib", runparams.nice); FileName try_in_file = makeAbsPath(database + ".bib", filePath()); bool not_from_texmf = try_in_file.isReadableFile(); // If the file has not been found, try with the real file name // (it might come from a child in a sub-directory) if (!not_from_texmf) { try_in_file = getBibfilePath(bit); if (try_in_file.isReadableFile()) { // Check if the file is in texmf FileName kpsefile(findtexfile(changeExtension(utf8input, "bib"), "bib", true)); not_from_texmf = kpsefile.empty() || kpsefile.absFileName() != try_in_file.absFileName(); if (not_from_texmf) // If this exists, make path relative to the master // FIXME Unicode database = removeExtension(prepareFileNameForLaTeX( to_utf8(makeRelPath(from_utf8(try_in_file.absFileName()), from_utf8(filePath()))), ".bib", runparams.nice)); } } if (!runparams.inComment && !runparams.dryrun && !runparams.nice && not_from_texmf) { // mangledFileName() needs the extension DocFileName const in_file = DocFileName(try_in_file); database = removeExtension(in_file.mangledFileName()); FileName const out_file = makeAbsPath(database + ".bib", masterBuffer()->temppath()); bool const success = in_file.copyTo(out_file); if (!success) { LYXERR0("Failed to copy '" << in_file << "' to '" << out_file << "'"); } } else if (!runparams.inComment && runparams.nice && not_from_texmf) { runparams.exportdata->addExternalFile(tex_format, try_in_file, database + ".bib"); if (!isValidLaTeXFileName(database)) { frontend::Alert::warning(_("Invalid filename"), _("The following filename will cause troubles " "when running the exported file through LaTeX: ") + from_utf8(database)); } if (!isValidDVIFileName(database)) { frontend::Alert::warning(_("Problematic filename for DVI"), _("The following filename can cause troubles " "when running the exported file through LaTeX " "and opening the resulting DVI: ") + from_utf8(database), true); } } if (add_extension) database += ".bib"; // FIXME UNICODE docstring const path = from_utf8(latex_path(database)); if (contains(path, ' ')) found_space = true; string enc; if (params().useBiblatex() && !params().bibFileEncoding(utf8input).empty()) enc = params().bibFileEncoding(utf8input); bool recorded = false; for (auto const & pe : res) { if (pe.first == path) { recorded = true; break; } } if (!recorded) res.push_back(make_pair(path, enc)); } // Check if there are spaces in the path and warn BibTeX users, if so. // (biber can cope with such paths) if (!prefixIs(runparams.bibtex_command, "biber")) { // Post this warning only once. static bool warned_about_spaces = false; if (!warned_about_spaces && runparams.nice && found_space) { warned_about_spaces = true; Alert::warning(_("Export Warning!"), _("There are spaces in the paths to your BibTeX databases.\n" "BibTeX will be unable to find them.")); } } return res; } string Buffer::layoutPos() const { return d->layout_position; } void Buffer::setLayoutPos(string const & path) { if (path.empty()) { d->layout_position.clear(); return; } LATTEST(FileName::isAbsolute(path)); d->layout_position = to_utf8(makeRelPath(from_utf8(path), from_utf8(filePath()))); if (d->layout_position.empty()) d->layout_position = "."; } bool Buffer::hasReadonlyFlag() const { return d->read_only; } bool Buffer::isReadonly() const { return hasReadonlyFlag() || notifiesExternalModification(); } void Buffer::setParent(Buffer const * buffer) { // We need to do some work here to avoid recursive parent structures. // This is the easy case. if (buffer == this) { LYXERR0("Ignoring attempt to set self as parent in\n" << fileName()); return; } // Now we check parents going upward, to make sure that IF we set the // parent as requested, we would not generate a recursive include. set sb; Buffer const * b = buffer; bool found_recursion = false; while (b) { if (sb.find(b) != sb.end()) { found_recursion = true; break; } sb.insert(b); b = b->parent(); } if (found_recursion) { LYXERR0("Ignoring attempt to set parent of\n" << fileName() << "\nto " << buffer->fileName() << "\nbecause that would create a recursive inclusion."); return; } // We should be safe now. d->setParent(buffer); updateMacros(); } Buffer const * Buffer::parent() const { return d->parent(); } ListOfBuffers Buffer::allRelatives() const { ListOfBuffers lb = masterBuffer()->getDescendants(); lb.push_front(const_cast(masterBuffer())); return lb; } Buffer const * Buffer::masterBuffer() const { Buffer const * const pbuf = d->parent(); if (!pbuf) return this; return pbuf->masterBuffer(); } bool Buffer::isChild(Buffer * child) const { return d->children_positions.find(child) != d->children_positions.end(); } DocIterator Buffer::firstChildPosition(Buffer const * child) { Impl::BufferPositionMap::iterator it; it = d->children_positions.find(child); if (it == d->children_positions.end()) return DocIterator(this); return it->second; } bool Buffer::hasChildren() const { return !d->children_positions.empty(); } void Buffer::collectChildren(ListOfBuffers & children, bool grand_children) const { // loop over children for (auto const & p : d->children_positions) { Buffer * child = const_cast(p.first); // No duplicates ListOfBuffers::const_iterator bit = find(children.begin(), children.end(), child); if (bit != children.end()) continue; children.push_back(child); if (grand_children) // there might be grandchildren child->collectChildren(children, true); } } ListOfBuffers Buffer::getChildren() const { ListOfBuffers v; collectChildren(v, false); // Make sure we have not included ourselves. ListOfBuffers::iterator bit = find(v.begin(), v.end(), this); if (bit != v.end()) { LYXERR0("Recursive include detected in `" << fileName() << "'."); v.erase(bit); } return v; } ListOfBuffers Buffer::getDescendants() const { ListOfBuffers v; collectChildren(v, true); // Make sure we have not included ourselves. ListOfBuffers::iterator bit = find(v.begin(), v.end(), this); if (bit != v.end()) { LYXERR0("Recursive include detected in `" << fileName() << "'."); v.erase(bit); } return v; } template typename M::const_iterator greatest_below(M & m, typename M::key_type const & x) { if (m.empty()) return m.end(); typename M::const_iterator it = m.lower_bound(x); if (it == m.begin()) return m.end(); --it; return it; } MacroData const * Buffer::Impl::getBufferMacro(docstring const & name, DocIterator const & pos) const { LYXERR(Debug::MACROS, "Searching for " << to_ascii(name) << " at " << pos); // if paragraphs have no macro context set, pos will be empty if (pos.empty()) return nullptr; // we haven't found anything yet DocIterator bestPos = owner_->par_iterator_begin(); MacroData const * bestData = nullptr; // find macro definitions for name NamePositionScopeMacroMap::const_iterator nameIt = macros.find(name); if (nameIt != macros.end()) { // find last definition in front of pos or at pos itself PositionScopeMacroMap::const_iterator it = greatest_below(nameIt->second, pos); if (it != nameIt->second.end()) { while (true) { // scope ends behind pos? if (pos < it->second.scope) { // Looks good, remember this. If there // is no external macro behind this, // we found the right one already. bestPos = it->first; bestData = &it->second.macro; break; } // try previous macro if there is one if (it == nameIt->second.begin()) break; --it; } } } // find macros in included files PositionScopeBufferMap::const_iterator it = greatest_below(position_to_children, pos); if (it == position_to_children.end()) // no children before return bestData; while (true) { // do we know something better (i.e. later) already? if (it->first < bestPos ) break; // scope ends behind pos? if (pos < it->second.scope && (cloned_buffer_ || theBufferList().isLoaded(it->second.buffer))) { // look for macro in external file macro_lock = true; MacroData const * data = it->second.buffer->getMacro(name, false); macro_lock = false; if (data) { bestPos = it->first; bestData = data; break; } } // try previous file if there is one if (it == position_to_children.begin()) break; --it; } // return the best macro we have found return bestData; } MacroData const * Buffer::getMacro(docstring const & name, DocIterator const & pos, bool global) const { if (d->macro_lock) return nullptr; // query buffer macros MacroData const * data = d->getBufferMacro(name, pos); if (data != nullptr) return data; // If there is a master buffer, query that Buffer const * const pbuf = d->parent(); if (pbuf) { d->macro_lock = true; MacroData const * macro = pbuf->getMacro( name, *this, false); d->macro_lock = false; if (macro) return macro; } if (global) { data = MacroTable::globalMacros().get(name); if (data != nullptr) return data; } return nullptr; } MacroData const * Buffer::getMacro(docstring const & name, bool global) const { // set scope end behind the last paragraph DocIterator scope = par_iterator_begin(); scope.pit() = scope.lastpit() + 1; return getMacro(name, scope, global); } MacroData const * Buffer::getMacro(docstring const & name, Buffer const & child, bool global) const { // look where the child buffer is included first Impl::BufferPositionMap::iterator it = d->children_positions.find(&child); if (it == d->children_positions.end()) return nullptr; // check for macros at the inclusion position return getMacro(name, it->second, global); } void Buffer::Impl::updateMacros(DocIterator & it, DocIterator & scope) { pit_type const lastpit = it.lastpit(); // look for macros in each paragraph while (it.pit() <= lastpit) { Paragraph & par = it.paragraph(); // iterate over the insets of the current paragraph for (auto const & insit : par.insetList()) { it.pos() = insit.pos; // is it a nested text inset? if (insit.inset->asInsetText()) { // Inset needs its own scope? InsetText const * itext = insit.inset->asInsetText(); bool newScope = itext->isMacroScope(); // scope which ends just behind the inset DocIterator insetScope = it; ++insetScope.pos(); // collect macros in inset it.push_back(CursorSlice(*insit.inset)); updateMacros(it, newScope ? insetScope : scope); it.pop_back(); continue; } if (insit.inset->asInsetTabular()) { CursorSlice slice(*insit.inset); size_t const numcells = slice.nargs(); for (; slice.idx() < numcells; slice.forwardIdx()) { it.push_back(slice); updateMacros(it, scope); it.pop_back(); } continue; } // is it an external file? if (insit.inset->lyxCode() == INCLUDE_CODE) { // get buffer of external file InsetInclude const & incinset = static_cast(*insit.inset); macro_lock = true; Buffer * child = incinset.loadIfNeeded(); macro_lock = false; if (!child) continue; // register its position, but only when it is // included first in the buffer children_positions.insert({child, it}); // register child with its scope position_to_children[it] = Impl::ScopeBuffer(scope, child); continue; } InsetMath * im = insit.inset->asInsetMath(); if (doing_export && im) { InsetMathHull * hull = im->asHullInset(); if (hull) hull->recordLocation(it); } if (insit.inset->lyxCode() != MATHMACRO_CODE) continue; // get macro data InsetMathMacroTemplate & macroTemplate = *insit.inset->asInsetMath()->asMacroTemplate(); MacroContext mc(owner_, it); macroTemplate.updateToContext(mc); // valid? bool valid = macroTemplate.validMacro(); // FIXME: Should be fixNameAndCheckIfValid() in fact, // then the BufferView's cursor will be invalid in // some cases which leads to crashes. if (!valid) continue; // register macro // FIXME (Abdel), I don't understand why we pass 'it' here // instead of 'macroTemplate' defined above... is this correct? macros[macroTemplate.name()][it] = Impl::ScopeMacro(scope, MacroData(owner_, it)); } // next paragraph it.pit()++; it.pos() = 0; } } void Buffer::updateMacros() const { if (d->macro_lock) return; LYXERR(Debug::MACROS, "updateMacro of " << d->filename.onlyFileName()); // start with empty table d->macros.clear(); d->children_positions.clear(); d->position_to_children.clear(); // Iterate over buffer, starting with first paragraph // The scope must be bigger than any lookup DocIterator // later. For the global lookup, lastpit+1 is used, hence // we use lastpit+2 here. DocIterator it = par_iterator_begin(); DocIterator outerScope = it; outerScope.pit() = outerScope.lastpit() + 2; d->updateMacros(it, outerScope); } void Buffer::getUsedBranches(std::list & result, bool const from_master) const { for (Inset const & it : inset()) { if (it.lyxCode() == BRANCH_CODE) { InsetBranch const & br = static_cast(it); docstring const name = br.branch(); if (!from_master && !params().branchlist().find(name)) result.push_back(name); else if (from_master && !masterBuffer()->params().branchlist().find(name)) result.push_back(name); continue; } if (it.lyxCode() == INCLUDE_CODE) { // get buffer of external file InsetInclude const & ins = static_cast(it); Buffer * child = ins.loadIfNeeded(); if (!child) continue; child->getUsedBranches(result, true); } } // remove duplicates result.unique(); } void Buffer::updateMacroInstances(UpdateType utype) const { LYXERR(Debug::MACROS, "updateMacroInstances for " << d->filename.onlyFileName()); DocIterator it = doc_iterator_begin(this); it.forwardInset(); DocIterator const end = doc_iterator_end(this); for (; it != end; it.forwardInset()) { // look for MathData cells in InsetMathNest insets InsetMath * minset = it.nextInset()->asInsetMath(); if (!minset) continue; // update macro in all cells of the InsetMathNest idx_type n = minset->nargs(); MacroContext mc = MacroContext(this, it); for (idx_type i = 0; i < n; ++i) { MathData & data = minset->cell(i); data.updateMacros(nullptr, mc, utype, 0); } } } void Buffer::listMacroNames(MacroNameSet & macros) const { if (d->macro_lock) return; d->macro_lock = true; // loop over macro names for (auto const & nameit : d->macros) macros.insert(nameit.first); // loop over children for (auto const & p : d->children_positions) { Buffer * child = const_cast(p.first); // The buffer might have been closed (see #10766). if (theBufferList().isLoaded(child)) child->listMacroNames(macros); } // call parent Buffer const * const pbuf = d->parent(); if (pbuf) pbuf->listMacroNames(macros); d->macro_lock = false; } void Buffer::listParentMacros(MacroSet & macros, LaTeXFeatures & features) const { Buffer const * const pbuf = d->parent(); if (!pbuf) return; MacroNameSet names; pbuf->listMacroNames(names); // resolve macros for (auto const & mit : names) { // defined? MacroData const * data = pbuf->getMacro(mit, *this, false); if (data) { macros.insert(data); // we cannot access the original InsetMathMacroTemplate anymore // here to calls validate method. So we do its work here manually. // FIXME: somehow make the template accessible here. if (data->optionals() > 0) features.require("xargs"); } } } Buffer::References & Buffer::getReferenceCache(docstring const & label) { if (d->parent()) return const_cast(masterBuffer())->getReferenceCache(label); RefCache::iterator it = d->ref_cache_.find(label); if (it != d->ref_cache_.end()) return it->second; static References const dummy_refs = References(); it = d->ref_cache_.insert( make_pair(label, dummy_refs)).first; return it->second; } Buffer::References const & Buffer::references(docstring const & label) const { return const_cast(this)->getReferenceCache(label); } void Buffer::addReference(docstring const & label, Inset * inset, ParIterator it) { References & refs = getReferenceCache(label); refs.push_back(make_pair(inset, it)); } void Buffer::setInsetLabel(docstring const & label, InsetLabel const * il, bool const active) { LabelInfo linfo; linfo.label = label; linfo.inset = il; linfo.active = active; masterBuffer()->d->label_cache_.push_back(linfo); } InsetLabel const * Buffer::insetLabel(docstring const & label, bool const active) const { for (auto const & rc : masterBuffer()->d->label_cache_) { if (rc.label == label && (rc.active || !active)) return rc.inset; } return nullptr; } bool Buffer::activeLabel(docstring const & label) const { return insetLabel(label, true) != nullptr; } void Buffer::clearReferenceCache() const { if (!d->parent()) { d->ref_cache_.clear(); d->label_cache_.clear(); } } void Buffer::changeRefsIfUnique(docstring const & from, docstring const & to) { //FIXME: This does not work for child documents yet. reloadBibInfoCache(); // Check if the label 'from' appears more than once vector labels; for (auto const & bibit : masterBibInfo()) labels.push_back(bibit.first); if (count(labels.begin(), labels.end(), from) > 1) return; string const paramName = "key"; UndoGroupHelper ugh(this); InsetIterator it = begin(inset()); for (; it; ++it) { if (it->lyxCode() != CITE_CODE) continue; InsetCommand * inset = it->asInsetCommand(); docstring const oldValue = inset->getParam(paramName); if (oldValue == from) { undo().recordUndo(CursorData(it)); inset->setParam(paramName, to); } } } // returns NULL if id-to-row conversion is unsupported unique_ptr Buffer::getSourceCode(odocstream & os, string const & format, pit_type par_begin, pit_type par_end, OutputWhat output, bool master) const { unique_ptr texrow; OutputParams runparams(¶ms().encoding()); runparams.nice = true; runparams.flavor = params().getOutputFlavor(format); runparams.linelen = lyxrc.plaintext_linelen; // No side effect of file copying and image conversion runparams.dryrun = true; // Some macros rely on font encoding runparams.main_fontenc = params().main_font_encoding(); // Use the right wrapping for the comment at the beginning of the generated // snippet, so that it is either valid LaTeX or valid XML (including HTML and DocBook). docstring comment_start = from_ascii("% "); docstring comment_end = from_ascii(""); if (runparams.flavor == Flavor::Html || runparams.flavor == Flavor::DocBook5) { comment_start = from_ascii(""); } if (output == CurrentParagraph) { runparams.par_begin = par_begin; runparams.par_end = par_end; if (par_begin + 1 == par_end) { os << comment_start << bformat(_("Preview source code for paragraph %1$d"), par_begin) << comment_end << "\n\n"; } else { os << comment_start << bformat(_("Preview source code from paragraph %1$s to %2$s"), convert(par_begin), convert(par_end - 1)) << comment_end << "\n\n"; } // output paragraphs if (runparams.flavor == Flavor::LyX) { Paragraph const & par = text().paragraphs()[par_begin]; ostringstream ods; depth_type dt = par.getDepth(); par.write(ods, params(), dt); os << from_utf8(ods.str()); } else if (runparams.flavor == Flavor::Html) { XMLStream xs(os); setMathFlavor(runparams); xhtmlParagraphs(text(), *this, xs, runparams); } else if (runparams.flavor == Flavor::Text) { bool dummy = false; // FIXME Handles only one paragraph, unlike the others. // Probably should have some routine with a signature like them. writePlaintextParagraph(*this, text().paragraphs()[par_begin], os, runparams, dummy); } else if (runparams.flavor == Flavor::DocBook5) { XMLStream xs{os}; docbookParagraphs(text(), *this, xs, runparams); } else { // If we are previewing a paragraph, even if this is the // child of some other buffer, let's cut the link here, // so that no concurring settings from the master // (e.g. branch state) interfere (see #8101). if (!master) d->ignore_parent = true; // We need to validate the Buffer params' features here // in order to know if we should output polyglossia // macros (instead of babel macros) LaTeXFeatures features(*this, params(), runparams); validate(features); runparams.use_polyglossia = features.usePolyglossia(); runparams.use_babel = features.useBabel(); runparams.use_hyperref = features.isRequired("hyperref"); // latex or literate otexstream ots(os); // output above ots.texrow().newlines(2); // the real stuff latexParagraphs(*this, text(), ots, runparams); texrow = ots.releaseTexRow(); // Restore the parenthood if (!master) d->ignore_parent = false; } } else { os << comment_start; if (output == FullSource) os << _("Preview source code"); else if (output == OnlyPreamble) os << _("Preview preamble"); else if (output == OnlyBody) os << _("Preview body"); os << comment_end; os << "\n\n"; if (runparams.flavor == Flavor::LyX) { ostringstream ods; if (output == FullSource) write(ods); else if (output == OnlyPreamble) params().writeFile(ods, this); else if (output == OnlyBody) text().write(ods); os << from_utf8(ods.str()); } else if (runparams.flavor == Flavor::Html) { writeLyXHTMLSource(os, runparams, output); } else if (runparams.flavor == Flavor::Text) { if (output == OnlyPreamble) os << "% "<< _("Plain text does not have a preamble."); else writePlaintextFile(*this, os, runparams); } else if (runparams.flavor == Flavor::DocBook5) { writeDocBookSource(os, runparams, output); } else { // latex or literate otexstream ots(os); // output above ots.texrow().newlines(2); if (master) runparams.is_child = true; updateBuffer(); writeLaTeXSource(ots, string(), runparams, output); texrow = ots.releaseTexRow(); } } return texrow; } ErrorList & Buffer::errorList(string const & type) const { return d->errorLists[type]; } void Buffer::updateTocItem(std::string const & type, DocIterator const & dit) const { if (d->gui_) d->gui_->updateTocItem(type, dit); } void Buffer::structureChanged() const { if (d->gui_) d->gui_->structureChanged(); } void Buffer::errors(string const & err, bool from_master) const { if (d->gui_) d->gui_->errors(err, from_master); } void Buffer::message(docstring const & msg) const { if (d->gui_) d->gui_->message(msg); } void Buffer::setBusy(bool on) const { if (d->gui_) d->gui_->setBusy(on); } void Buffer::updateTitles() const { if (d->wa_) d->wa_->updateTitles(); } void Buffer::scheduleRedrawWorkAreas() const { if (d->wa_) d->wa_->scheduleRedraw(); } void Buffer::resetAutosaveTimers() const { if (d->gui_) d->gui_->resetAutosaveTimers(); } bool Buffer::hasGuiDelegate() const { return d->gui_; } void Buffer::setGuiDelegate(frontend::GuiBufferDelegate * gui) { d->gui_ = gui; } FileName Buffer::getEmergencyFileName() const { return FileName(d->filename.absFileName() + ".emergency"); } FileName Buffer::getAutosaveFileName() const { // if the document is unnamed try to save in the backup dir, else // in the default document path, and as a last try in the filePath, // which will most often be the temporary directory string fpath; if (isUnnamed()) fpath = lyxrc.backupdir_path.empty() ? lyxrc.document_path : lyxrc.backupdir_path; if (!isUnnamed() || fpath.empty() || !FileName(fpath).exists()) fpath = filePath(); string const fname = "#" + d->filename.onlyFileName() + "#"; return makeAbsPath(fname, fpath); } void Buffer::removeAutosaveFile() const { FileName const f = getAutosaveFileName(); if (f.exists()) f.removeFile(); } void Buffer::moveAutosaveFile(FileName const & oldauto) const { FileName const newauto = getAutosaveFileName(); oldauto.refresh(); if (newauto != oldauto && oldauto.exists()) if (!oldauto.moveTo(newauto)) LYXERR0("Unable to move autosave file `" << oldauto << "'!"); } bool Buffer::autoSave() const { Buffer const * buf = d->cloned_buffer_ ? d->cloned_buffer_ : this; if (buf->d->bak_clean || hasReadonlyFlag()) return true; message(_("Autosaving current document...")); buf->d->bak_clean = true; FileName const fname = getAutosaveFileName(); LASSERT(d->cloned_buffer_, return false); // If this buffer is cloned, we assume that // we are running in a separate thread already. TempFile tempfile("lyxautoXXXXXX.lyx"); tempfile.setAutoRemove(false); FileName const tmp_ret = tempfile.name(); if (!tmp_ret.empty()) { writeFile(tmp_ret); // assume successful write of tmp_ret if (tmp_ret.moveTo(fname)) return true; } // failed to write/rename tmp_ret so try writing direct return writeFile(fname); } void Buffer::setExportStatus(bool e) const { d->doing_export = e; ListOfBuffers clist = getDescendants(); for (auto const & bit : clist) bit->d->doing_export = e; } bool Buffer::isExporting() const { return d->doing_export; } Buffer::ExportStatus Buffer::doExport(string const & target, bool put_in_tempdir) const { string result_file; return doExport(target, put_in_tempdir, result_file); } Buffer::ExportStatus Buffer::doExport(string const & target, bool put_in_tempdir, string & result_file) const { bool const update_unincluded = params().maintain_unincluded_children != BufferParams::CM_None && !params().getIncludedChildren().empty(); // (1) export with all included children (omit \includeonly) if (update_unincluded) { ExportStatus const status = doExport(target, put_in_tempdir, true, result_file); if (status != ExportSuccess) return status; } // (2) export with included children only return doExport(target, put_in_tempdir, false, result_file); } void Buffer::setMathFlavor(OutputParams & op) const { // Passes the way to generate formulae to the XHTML output code. // In particular, this function has no impact on the DocBook code, as it // uses another mechanism to handle math flavours. switch (params().html_math_output) { case BufferParams::MathML: op.math_flavor = OutputParams::MathAsMathML; break; case BufferParams::HTML: op.math_flavor = OutputParams::MathAsHTML; break; case BufferParams::Images: op.math_flavor = OutputParams::MathAsImages; break; case BufferParams::LaTeX: op.math_flavor = OutputParams::MathAsLaTeX; break; } } Buffer::ExportStatus Buffer::doExport(string const & target, bool put_in_tempdir, bool includeall, string & result_file) const { LYXERR(Debug::FILES, "target=" << target); OutputParams runparams(¶ms().encoding()); string format = target; string dest_filename; size_t pos = target.find(' '); if (pos != string::npos) { dest_filename = target.substr(pos + 1, target.length() - pos - 1); format = target.substr(0, pos); if (format == "default") format = params().getDefaultOutputFormat(); runparams.export_folder = FileName(dest_filename).onlyPath().realPath(); FileName(dest_filename).onlyPath().createPath(); LYXERR(Debug::FILES, "format=" << format << ", dest_filename=" << dest_filename << ", export_folder=" << runparams.export_folder); } MarkAsExporting exporting(this); string backend_format; runparams.flavor = Flavor::LaTeX; runparams.linelen = lyxrc.plaintext_linelen; runparams.includeall = includeall; vector backs = params().backends(); Converters converters = theConverters(); bool need_nice_file = false; if (find(backs.begin(), backs.end(), format) == backs.end()) { // Get the shortest path to format converters.buildGraph(); Graph::EdgePath path; for (string const & sit : backs) { Graph::EdgePath p = converters.getPath(sit, format); if (!p.empty() && (path.empty() || p.size() < path.size())) { backend_format = sit; path = p; } } if (path.empty()) { if (!put_in_tempdir) { // Only show this alert if this is an export to a non-temporary // file (not for previewing). docstring s = bformat(_("No information for exporting the format %1$s."), translateIfPossible(theFormats().prettyName(format))); if (format == "pdf4") s += "\n" + bformat(_("Hint: use non-TeX fonts or set input encoding" " to '%1$s'"), from_utf8(encodings.fromLyXName("ascii")->guiName())); Alert::error(_("Couldn't export file"), s); } return ExportNoPathToFormat; } runparams.flavor = converters.getFlavor(path, this); runparams.hyperref_driver = converters.getHyperrefDriver(path); for (auto const & edge : path) if (theConverters().get(edge).nice()) { need_nice_file = true; break; } } else { backend_format = format; LYXERR(Debug::FILES, "backend_format=" << backend_format); // FIXME: Don't hardcode format names here, but use a flag if (backend_format == "pdflatex") runparams.flavor = Flavor::PdfLaTeX; else if (backend_format == "luatex") runparams.flavor = Flavor::LuaTeX; else if (backend_format == "dviluatex") runparams.flavor = Flavor::DviLuaTeX; else if (backend_format == "xetex") runparams.flavor = Flavor::XeTeX; } string filename = latexName(false); filename = addName(temppath(), filename); filename = changeExtension(filename, theFormats().extension(backend_format)); LYXERR(Debug::FILES, "filename=" << filename); // Plain text backend if (backend_format == "text") { runparams.flavor = Flavor::Text; try { writePlaintextFile(*this, FileName(filename), runparams); } catch (ConversionException const &) { return ExportCancel; } } // HTML backend else if (backend_format == "xhtml") { runparams.flavor = Flavor::Html; setMathFlavor(runparams); if (makeLyXHTMLFile(FileName(filename), runparams) == ExportKilled) return ExportKilled; } else if (backend_format == "lyx") writeFile(FileName(filename)); // DocBook backend else if (backend_format == "docbook5") { runparams.flavor = Flavor::DocBook5; runparams.nice = false; if (makeDocBookFile(FileName(filename), runparams) == ExportKilled) return ExportKilled; } // LaTeX backend else if (backend_format == format || need_nice_file) { runparams.nice = true; ExportStatus const retval = makeLaTeXFile(FileName(filename), string(), runparams); if (retval == ExportKilled) return ExportKilled; if (d->cloned_buffer_) d->cloned_buffer_->d->errorLists["Export"] = d->errorLists["Export"]; if (retval != ExportSuccess) return retval; } else if (!lyxrc.tex_allows_spaces && contains(filePath(), ' ')) { Alert::error(_("File name error"), bformat(_("The directory path to the document\n%1$s\n" "contains spaces, but your TeX installation does " "not allow them. You should save the file to a directory " "whose name does not contain spaces."), from_utf8(filePath()))); return ExportTexPathHasSpaces; } else { runparams.nice = false; ExportStatus const retval = makeLaTeXFile(FileName(filename), filePath(), runparams); if (retval == ExportKilled) return ExportKilled; if (d->cloned_buffer_) d->cloned_buffer_->d->errorLists["Export"] = d->errorLists["Export"]; if (retval != ExportSuccess) return ExportError; } string const error_type = (format == "program") ? "Build" : params().bufferFormat(); ErrorList & error_list = d->errorLists[error_type]; string const ext = theFormats().extension(format); FileName const tmp_result_file(changeExtension(filename, ext)); Converters::RetVal const retval = converters.convert(this, FileName(filename), tmp_result_file, FileName(absFileName()), backend_format, format, error_list, Converters::none, includeall); if (retval == Converters::KILLED) return ExportCancel; bool success = (retval == Converters::SUCCESS); // Emit the signal to show the error list or copy it back to the // cloned Buffer so that it can be emitted afterwards. if (format != backend_format) { if (runparams.silent) error_list.clear(); else if (d->cloned_buffer_) d->cloned_buffer_->d->errorLists[error_type] = d->errorLists[error_type]; else errors(error_type); // also to the children, in case of master-buffer-view ListOfBuffers clist = getDescendants(); for (auto const & bit : clist) { if (runparams.silent) bit->d->errorLists[error_type].clear(); else if (d->cloned_buffer_) { // Enable reverse search by copying back the // texrow object to the cloned buffer. // FIXME: this is not thread safe. bit->d->cloned_buffer_->d->texrow = bit->d->texrow; bit->d->cloned_buffer_->d->errorLists[error_type] = bit->d->errorLists[error_type]; } else bit->errors(error_type, true); } } if (d->cloned_buffer_) { // Enable reverse dvi or pdf to work by copying back the texrow // object to the cloned buffer. // FIXME: There is a possibility of concurrent access to texrow // here from the main GUI thread that should be securized. d->cloned_buffer_->d->texrow = d->texrow; string const err_type = params().bufferFormat(); d->cloned_buffer_->d->errorLists[error_type] = d->errorLists[err_type]; } if (put_in_tempdir) { result_file = tmp_result_file.absFileName(); return success ? ExportSuccess : ExportConverterError; } if (dest_filename.empty()) result_file = changeExtension(d->exportFileName().absFileName(), ext); else result_file = dest_filename; // We need to copy referenced files (e.g. included graphics // if format == "dvi") to the result dir. vector const extfiles = runparams.exportdata->externalFiles(format); string const dest = runparams.export_folder.empty() ? onlyPath(result_file) : runparams.export_folder; bool use_force = use_gui ? lyxrc.export_overwrite == ALL_FILES : force_overwrite == ALL_FILES; CopyStatus status = use_force ? FORCE : SUCCESS; for (ExportedFile const & exp : extfiles) { if (status == CANCEL) { message(_("Document export cancelled.")); return ExportCancel; } string const fmt = theFormats().getFormatFromFile(exp.sourceName); string fixedName = exp.exportName; if (!runparams.export_folder.empty()) { // Relative pathnames starting with ../ will be sanitized // if exporting to a different folder while (fixedName.substr(0, 3) == "../") fixedName = fixedName.substr(3, fixedName.length() - 3); } FileName fixedFileName = makeAbsPath(fixedName, dest); fixedFileName.onlyPath().createPath(); status = copyFile(fmt, exp.sourceName, fixedFileName, exp.exportName, status == FORCE, runparams.export_folder.empty()); } if (tmp_result_file.exists()) { // Finally copy the main file use_force = use_gui ? lyxrc.export_overwrite != NO_FILES : force_overwrite != NO_FILES; if (status == SUCCESS && use_force) status = FORCE; status = copyFile(format, tmp_result_file, FileName(result_file), result_file, status == FORCE); if (status == CANCEL) { message(_("Document export cancelled.")); return ExportCancel; } else { message(bformat(_("Document exported as %1$s " "to file `%2$s'"), translateIfPossible(theFormats().prettyName(format)), makeDisplayPath(result_file))); } } else { // This must be a dummy converter like fax (bug 1888) message(bformat(_("Document exported as %1$s"), translateIfPossible(theFormats().prettyName(format)))); } return success ? ExportSuccess : ExportConverterError; } Buffer::ExportStatus Buffer::preview(string const & format) const { bool const update_unincluded = params().maintain_unincluded_children != BufferParams::CM_None && !params().getIncludedChildren().empty(); return preview(format, update_unincluded); } Buffer::ExportStatus Buffer::preview(string const & format, bool includeall) const { MarkAsExporting exporting(this); string result_file; // (1) export with all included children (omit \includeonly) if (includeall) { ExportStatus const status = doExport(format, true, true, result_file); if (status != ExportSuccess) return status; } // (2) export with included children only ExportStatus const status = doExport(format, true, false, result_file); FileName const previewFile(result_file); Impl * theimpl = isClone() ? d->cloned_buffer_->d : d; theimpl->preview_file_ = previewFile; theimpl->preview_format_ = format; theimpl->require_fresh_start_ = (status != ExportSuccess); if (status != ExportSuccess) return status; if (previewFile.exists()) return theFormats().view(*this, previewFile, format) ? PreviewSuccess : PreviewError; // Successful export but no output file? // Probably a bug in error detection. LATTEST(status != ExportSuccess); return status; } Buffer::ReadStatus Buffer::extractFromVC() { bool const found = LyXVC::file_not_found_hook(d->filename); if (!found) return ReadFileNotFound; if (!d->filename.isReadableFile()) return ReadVCError; return ReadSuccess; } Buffer::ReadStatus Buffer::loadEmergency() { FileName const emergencyFile = getEmergencyFileName(); if (!emergencyFile.exists() || emergencyFile.lastModified() <= d->filename.lastModified()) return ReadFileNotFound; docstring const file = makeDisplayPath(d->filename.absFileName(), 20); docstring const text = bformat(_("An emergency save of the document " "%1$s exists.\n\nRecover emergency save?"), file); int const load_emerg = Alert::prompt(_("Load emergency save?"), text, 0, 3, _("&Recover"), _("&Load Original"), _("&Only show difference"), _("&Cancel")); switch (load_emerg) { case 0: { docstring str; ReadStatus const ret_llf = loadThisLyXFile(emergencyFile); bool const success = (ret_llf == ReadSuccess); if (success) { if (hasReadonlyFlag()) { Alert::warning(_("File is read-only"), bformat(_("An emergency file is successfully loaded, " "but the original file %1$s is marked read-only. " "Please make sure to save the document as a different " "file."), from_utf8(d->filename.absFileName()))); } markDirty(); lyxvc().file_found_hook(d->filename); str = _("Document was successfully recovered."); } else str = _("Document was NOT successfully recovered."); str += "\n\n" + bformat(_("Remove emergency file now?\n(%1$s)"), makeDisplayPath(emergencyFile.absFileName())); int const del_emerg = Alert::prompt(_("Delete emergency file?"), str, 1, 1, _("&Remove"), _("&Keep")); if (del_emerg == 0) { emergencyFile.removeFile(); if (success) Alert::warning(_("Emergency file deleted"), _("Do not forget to save your file now!"), true); } return success ? ReadSuccess : ReadEmergencyFailure; } case 1: { int const del_emerg = Alert::prompt(_("Delete emergency file?"), _("Remove emergency file now?"), 1, 1, _("&Remove"), _("&Keep")); if (del_emerg == 0) emergencyFile.removeFile(); else { // See bug #11464 FileName newname; string const ename = emergencyFile.absFileName(); bool noname = true; // Surely we can find one in 100 tries? for (int i = 1; i < 100; ++i) { newname.set(ename + to_string(i) + ".lyx"); if (!newname.exists()) { noname = false; break; } } if (!noname) { // renameTo returns true on success. So inverting that // will give us true if we fail. noname = !emergencyFile.renameTo(newname); } if (noname) { Alert::warning(_("Can't rename emergency file!"), _("LyX was unable to rename the emergency file. " "You should do so manually. Otherwise, you will be " "asked about it again the next time you try to load " "this file, and may over-write your own work.")); } else { Alert::warning(_("Emergency File Renamed"), bformat(_("Emergency file renamed as:\n %1$s"), from_utf8(newname.onlyFileName()))); } } return ReadOriginal; } case 2: { string const f1 = d->filename.absFileName(); string const f2 = emergencyFile.absFileName(); if (loadThisLyXFile(d->filename) != ReadSuccess) return ReadCancel; string const par = "compare run-blocking " + quoteName(f1) + " " + quoteName(f2); LYXERR(Debug::FILES, par << "\n"); lyx::dispatch(FuncRequest(LFUN_DIALOG_SHOW, par)); //release the emergency buffer loaded by compare Buffer * emerBuffer = theBufferList().getBuffer(emergencyFile); if (emerBuffer) theBufferList().release(emerBuffer); return ReadCancel; //Release the buffer of Original immediatelly } default: break; } return ReadCancel; } Buffer::ReadStatus Buffer::loadAutosave() { // Now check if autosave file is newer. FileName const autosaveFile = getAutosaveFileName(); if (!autosaveFile.exists() || autosaveFile.lastModified() <= d->filename.lastModified()) return ReadFileNotFound; docstring const file = makeDisplayPath(d->filename.absFileName(), 20); docstring const text = bformat(_("The backup of the document %1$s " "is newer.\n\nLoad the backup instead?"), file); int const ret = Alert::prompt(_("Load backup?"), text, 0, 2, _("&Load backup"), _("Load &original"), _("&Cancel")); switch (ret) { case 0: { ReadStatus const ret_llf = loadThisLyXFile(autosaveFile); // the file is not saved if we load the autosave file. if (ret_llf == ReadSuccess) { if (hasReadonlyFlag()) { Alert::warning(_("File is read-only"), bformat(_("A backup file is successfully loaded, " "but the original file %1$s is marked read-only. " "Please make sure to save the document as a " "different file."), from_utf8(d->filename.absFileName()))); } markDirty(); lyxvc().file_found_hook(d->filename); return ReadSuccess; } return ReadAutosaveFailure; } case 1: // Here we delete the autosave autosaveFile.removeFile(); return ReadOriginal; default: break; } return ReadCancel; } Buffer::ReadStatus Buffer::loadLyXFile() { if (!d->filename.isReadableFile()) { ReadStatus const ret_rvc = extractFromVC(); if (ret_rvc != ReadSuccess) return ret_rvc; } ReadStatus const ret_re = loadEmergency(); if (ret_re == ReadSuccess || ret_re == ReadCancel) return ret_re; ReadStatus const ret_ra = loadAutosave(); if (ret_ra == ReadSuccess || ret_ra == ReadCancel) return ret_ra; return loadThisLyXFile(d->filename); } Buffer::ReadStatus Buffer::loadThisLyXFile(FileName const & fn) { return readFile(fn); } void Buffer::Impl::traverseErrors(TeXErrors::Errors::const_iterator err, TeXErrors::Errors::const_iterator end, ErrorList & errorList) const { for (; err != end; ++err) { TexRow::TextEntry start = TexRow::text_none, end = TexRow::text_none; int errorRow = err->error_in_line; Buffer const * buf = nullptr; Impl const * p = this; if (err->child_name.empty()) tie(start, end) = p->texrow.getEntriesFromRow(errorRow); else { // The error occurred in a child for (Buffer const * child : owner_->getDescendants()) { string const child_name = DocFileName(changeExtension(child->absFileName(), "tex")). mangledFileName(); if (err->child_name != child_name) continue; tie(start, end) = child->d->texrow.getEntriesFromRow(errorRow); if (!TexRow::isNone(start)) { buf = this->cloned_buffer_ ? child->d->cloned_buffer_->d->owner_ : child->d->owner_; p = child->d; break; } } } errorList.push_back(ErrorItem(err->error_desc, err->error_text, start, end, buf)); } } void Buffer::bufferErrors(TeXErrors const & terr, ErrorList & errorList) const { TeXErrors::Errors::const_iterator err = terr.begin(); TeXErrors::Errors::const_iterator end = terr.end(); d->traverseErrors(err, end, errorList); } void Buffer::bufferRefs(TeXErrors const & terr, ErrorList & errorList) const { TeXErrors::Errors::const_iterator err = terr.begin_ref(); TeXErrors::Errors::const_iterator end = terr.end_ref(); d->traverseErrors(err, end, errorList); } void Buffer::updateBuffer() const { updateBuffer(UpdateMaster, InternalUpdate); d->need_update = false; } void Buffer::updateBuffer(UpdateScope scope, UpdateType utype) const { LBUFERR(!text().paragraphs().empty()); // This can be called when loading a file, so that there be no // open undo group. UndoGroupHelper ugh(const_cast(this)); // Use the master text class also for child documents Buffer const * const master = masterBuffer(); DocumentClass const & textclass = master->params().documentClass(); docstring_list old_bibfiles; // Do this only if we are the top-level Buffer. We also need to account // for the case of a previewed child with ignored parent here. if (master == this && !d->ignore_parent) { textclass.counters().reset(from_ascii("bibitem")); reloadBibInfoCache(); // we will re-read this cache as we go through, but we need // to know whether it's changed to know whether we need to // update the bibinfo cache. old_bibfiles = d->bibfiles_cache_; d->bibfiles_cache_.clear(); } // keep the buffers to be children in this set. If the call from the // master comes back we can see which of them were actually seen (i.e. // via an InsetInclude). The remaining ones in the set need still be updated. static std::set bufToUpdate; if (scope == UpdateMaster) { // If this is a child document start with the master if (master != this) { bufToUpdate.insert(this); master->updateBuffer(UpdateMaster, utype); // If the master buffer has no gui associated with it, then the TocModel is // not updated during the updateBuffer call and TocModel::toc_ is invalid // (bug 5699). The same happens if the master buffer is open in a different // window. This test catches both possibilities. // See: https://marc.info/?l=lyx-devel&m=138590578911716&w=2 // There remains a problem here: If there is another child open in yet a third // window, that TOC is not updated. So some more general solution is needed at // some point. if (master->d->gui_ != d->gui_) structureChanged(); // was buf referenced from the master (i.e. not in bufToUpdate anymore)? if (bufToUpdate.find(this) == bufToUpdate.end()) return; } // start over the counters in the master textclass.counters().reset(); } // update will be done below for this buffer bufToUpdate.erase(this); // update all caches clearReferenceCache(); updateMacros(); Buffer & cbuf = const_cast(*this); // if we are reloading, then we could have a dangling TOC, // in effect. so we need to go ahead and reset, even though // we will do so again when we rebuild the TOC later. cbuf.tocBackend().reset(); // do the real work ParIterator parit = cbuf.par_iterator_begin(); if (scope == UpdateMaster) clearIncludeList(); updateBuffer(parit, utype); // If this document has siblings, then update the TocBackend later. The // reason is to ensure that later siblings are up to date when e.g. the // broken or not status of references is computed. The update is called // in InsetInclude::addToToc. if (master != this) return; // if the bibfiles changed, the cache of bibinfo is invalid docstring_list new_bibfiles = d->bibfiles_cache_; // this is a trick to determine whether the two vectors have // the same elements. sort(new_bibfiles.begin(), new_bibfiles.end()); sort(old_bibfiles.begin(), old_bibfiles.end()); if (old_bibfiles != new_bibfiles) { LYXERR(Debug::FILES, "Reloading bibinfo cache."); invalidateBibinfoCache(); reloadBibInfoCache(); // We relied upon the bibinfo cache when recalculating labels. But that // cache was invalid, although we didn't find that out until now. So we // have to do it all again. // That said, the only thing we really need to do is update the citation // labels. Nothing else will have changed. So we could create a new // UpdateType that would signal that fact, if we needed to do so. parit = cbuf.par_iterator_begin(); // we will be re-doing the counters and references and such. textclass.counters().reset(); clearReferenceCache(); // we should not need to do this again? // updateMacros(); updateBuffer(parit, utype); // this will already have been done by reloadBibInfoCache(); // d->bibinfo_cache_valid_ = true; } else { LYXERR(Debug::FILES, "Bibfiles unchanged."); // this is also set to true on the other path, by reloadBibInfoCache. d->bibinfo_cache_valid_ = true; } d->cite_labels_valid_ = true; /// FIXME: Perf clearIncludeList(); cbuf.tocBackend().update(true, utype); if (scope == UpdateMaster) cbuf.structureChanged(); d->need_update = false; } static depth_type getDepth(DocIterator const & it) { depth_type depth = 0; for (size_t i = 0 ; i < it.depth() ; ++i) if (!it[i].inset().inMathed()) depth += it[i].paragraph().getDepth() + 1; // remove 1 since the outer inset does not count // we should have at least one non-math inset, so // depth should nevery be 0. but maybe it is worth // marking this, just in case. LATTEST(depth > 0); // coverity[INTEGER_OVERFLOW] return depth - 1; } static depth_type getItemDepth(ParIterator const & it) { Paragraph const & par = *it; LabelType const labeltype = par.layout().labeltype; if (labeltype != LABEL_ENUMERATE && labeltype != LABEL_ITEMIZE) return 0; // this will hold the lowest depth encountered up to now. depth_type min_depth = getDepth(it); ParIterator prev_it = it; while (true) { if (prev_it.pit()) --prev_it.top().pit(); else { // start of nested inset: go to outer par prev_it.pop_back(); if (prev_it.empty()) { // start of document: nothing to do return 0; } } // We search for the first paragraph with same label // that is not more deeply nested. Paragraph & prev_par = *prev_it; depth_type const prev_depth = getDepth(prev_it); if (labeltype == prev_par.layout().labeltype) { if (prev_depth < min_depth) return prev_par.itemdepth + 1; if (prev_depth == min_depth) return prev_par.itemdepth; } min_depth = min(min_depth, prev_depth); // small optimization: if we are at depth 0, we won't // find anything else if (prev_depth == 0) return 0; } } static bool needEnumCounterReset(ParIterator const & it) { Paragraph const & par = *it; LASSERT(par.layout().labeltype == LABEL_ENUMERATE, return false); depth_type const cur_depth = par.getDepth(); ParIterator prev_it = it; while (prev_it.pit()) { --prev_it.top().pit(); Paragraph const & prev_par = *prev_it; if (prev_par.getDepth() <= cur_depth) return prev_par.layout().name() != par.layout().name(); } // start of nested inset: reset return true; } // set the label of a paragraph. This includes the counters. void Buffer::Impl::setLabel(ParIterator & it, UpdateType utype) const { BufferParams const & bp = owner_->masterBuffer()->params(); DocumentClass const & textclass = bp.documentClass(); Paragraph & par = it.paragraph(); Layout const & layout = par.layout(); Counters & counters = textclass.counters(); if (par.params().startOfAppendix()) { // We want to reset the counter corresponding to toplevel sectioning Layout const & lay = textclass.getTOCLayout(); docstring const cnt = lay.counter; if (!cnt.empty()) counters.reset(cnt); counters.appendix(true); } par.params().appendix(counters.appendix()); // Compute the item depth of the paragraph par.itemdepth = getItemDepth(it); if (layout.margintype == MARGIN_MANUAL) { if (par.params().labelWidthString().empty()) par.params().labelWidthString(par.expandLabel(layout, bp)); } else if (layout.latextype == LATEX_BIB_ENVIRONMENT) { // we do not need to do anything here, since the empty case is // handled during export. } else { par.params().labelWidthString(docstring()); } switch(layout.labeltype) { case LABEL_ITEMIZE: { par.params().labelString( (par.itemdepth < 4) ? bp.user_defined_bullet(par.itemdepth).getLabel() // Display fallback for too deeply nested items : bformat(from_ascii("[?%1$d]"), int(par.itemdepth + 1))); break; } case LABEL_ENUMERATE: { docstring enumcounter = layout.counter.empty() ? from_ascii("enum") : layout.counter; switch (par.itemdepth) { case 2: enumcounter += 'i'; // fall through case 1: enumcounter += 'i'; // fall through case 0: enumcounter += 'i'; break; case 3: enumcounter += "iv"; break; default: // not a valid enumdepth... break; } if (needEnumCounterReset(it)) { // Increase the parent counter? if (layout.stepparentcounter) counters.stepParent(enumcounter, utype); // Maybe we have to reset the enumeration counter. if (!layout.resumecounter) counters.reset(enumcounter); } counters.step(enumcounter, utype); string const & lang = par.getParLanguage(bp)->code(); par.params().labelString(counters.theCounter(enumcounter, lang)); break; } case LABEL_SENSITIVE: { string const & type = counters.current_float(); docstring full_label; if (type.empty()) full_label = owner_->B_("Senseless!!! "); else { docstring name = owner_->B_(textclass.floats().getType(type).name()); if (counters.hasCounter(from_utf8(type))) { string const & lang = par.getParLanguage(bp)->code(); counters.step(from_utf8(type), utype); full_label = bformat(from_ascii("%1$s %2$s:"), name, counters.theCounter(from_utf8(type), lang)); } else full_label = bformat(from_ascii("%1$s #:"), name); } par.params().labelString(full_label); break; } case LABEL_NO_LABEL: par.params().labelString(docstring()); break; case LABEL_ABOVE: case LABEL_CENTERED: case LABEL_STATIC: { docstring const & lcounter = layout.counter; if (!lcounter.empty()) { if (layout.toclevel <= bp.secnumdepth && (layout.latextype != LATEX_ENVIRONMENT || it.text()->isFirstInSequence(it.pit()))) { if (counters.hasCounter(lcounter)) counters.step(lcounter, utype); par.params().labelString(par.expandLabel(layout, bp)); } else par.params().labelString(docstring()); } else par.params().labelString(par.expandLabel(layout, bp)); break; } case LABEL_MANUAL: case LABEL_BIBLIO: par.params().labelString(par.expandLabel(layout, bp)); } } void Buffer::updateBuffer(ParIterator & parit, UpdateType utype, bool const deleted) const { // if fomatted references are shown in workarea update buffer accordingly if (params().use_formatted_ref) utype = OutputUpdate; pushIncludedBuffer(this); // LASSERT: Is it safe to continue here, or should we just return? LASSERT(parit.pit() == 0, /**/); // Set the position of the text in the buffer to be able // to resolve macros in it. parit.text()->setMacrocontextPosition(parit); depth_type maxdepth = 0; pit_type const lastpit = parit.lastpit(); bool changed = false; for ( ; parit.pit() <= lastpit ; ++parit.pit()) { // reduce depth if necessary if (parit->params().depth() > maxdepth) { /** FIXME: this function is const, but * nevertheless it modifies the buffer. To be * cleaner, one should modify the buffer in * another function, which is actually * non-const. This would however be costly in * terms of code duplication. */ CursorData(parit).recordUndo(); parit->params().depth(maxdepth); } maxdepth = parit->getMaxDepthAfter(); if (utype == OutputUpdate) { // track the active counters // we have to do this for the master buffer, since the local // buffer isn't tracking anything. masterBuffer()->params().documentClass().counters(). setActiveLayout(parit->layout()); } // set the counter for this paragraph d->setLabel(parit, utype); // now the insets for (auto const & insit : parit->insetList()) { parit.pos() = insit.pos; insit.inset->updateBuffer(parit, utype, deleted || parit->isDeleted(insit.pos)); changed |= insit.inset->isChanged(); } // are there changes in this paragraph? changed |= parit->isChanged(); } // set change indicator for the inset (or the cell that the iterator // points to, if applicable). parit.text()->inset().isChanged(changed); popIncludedBuffer(); } void Buffer::forceUpdate() const { d->need_update = true; } bool Buffer::needUpdate() const { return d->need_update; } int Buffer::spellCheck(DocIterator & from, DocIterator & to, WordLangTuple & word_lang, docstring_list & suggestions) const { int progress = 0; WordLangTuple wl; suggestions.clear(); word_lang = WordLangTuple(); bool const to_end = to.empty(); DocIterator const end = to_end ? doc_iterator_end(this) : to; // OK, we start from here. for (; from != end; from.forwardPos()) { // This skips all insets with spell check disabled. while (!from.allowSpellCheck()) { from.pop_back(); from.pos()++; } // If from is at the end of the document (which is possible // when "from" was changed above) LyX will crash later otherwise. if (from.atEnd() || (!to_end && from >= end)) break; to = from; from.paragraph().spellCheck(); SpellChecker::Result res = from.paragraph().spellCheck(from.pos(), to.pos(), wl, suggestions); if (SpellChecker::misspelled(res)) { word_lang = wl; break; } // Do not increase progress when from == to, otherwise the word // count will be wrong. if (from != to) { from = to; ++progress; } } return progress; } void Buffer::requestSpellcheck() { ParagraphList::iterator pit = paragraphs().begin(); ParagraphList::iterator pend = paragraphs().end(); for (; pit != pend; ++pit) pit->requestSpellCheck(); } void Buffer::Impl::updateStatistics(DocIterator & from, DocIterator & to, bool skipNoOutput) { bool inword = false; word_count_ = 0; char_count_ = 0; blank_count_ = 0; for (DocIterator dit = from ; dit != to && !dit.atEnd(); ) { if (!dit.inTexted()) { dit.forwardPos(); continue; } Paragraph const & par = dit.paragraph(); pos_type const pos = dit.pos(); // Copied and adapted from isWordSeparator() in Paragraph if (pos == dit.lastpos()) { inword = false; } else { Inset const * ins = par.getInset(pos); if (ins && skipNoOutput && !ins->producesOutput()) { // skip this inset ++dit.top().pos(); // stop if end of range was skipped if (!to.atEnd() && dit >= to) break; continue; } else if (!par.isDeleted(pos)) { if (par.isWordSeparator(pos)) inword = false; else if (!inword) { ++word_count_; inword = true; } if (ins && ins->isLetter()) { odocstringstream os; ins->toString(os); char_count_ += os.str().length(); } else if (ins && ins->isSpace()) ++blank_count_; else if (ins) { pair words = ins->isWords(); char_count_ += words.first; word_count_ += words.second; inword = false; } else { char_type const c = par.getChar(pos); if (isPrintableNonspace(c)) ++char_count_; else if (isSpace(c)) ++blank_count_; } } } dit.forwardPos(); } } void Buffer::updateStatistics(DocIterator & from, DocIterator & to, bool skipNoOutput) const { d->updateStatistics(from, to, skipNoOutput); } int Buffer::wordCount() const { return d->wordCount(); } int Buffer::charCount(bool with_blanks) const { return d->charCount(with_blanks); } bool Buffer::areChangesPresent() const { return inset().isChanged(); } Buffer::ReadStatus Buffer::reload() { setBusy(true); // c.f. bug https://www.lyx.org/trac/ticket/6587 removeAutosaveFile(); // e.g., read-only status could have changed due to version control d->filename.refresh(); docstring const disp_fn = makeDisplayPath(d->filename.absFileName()); // clear parent. this will get reset if need be. d->setParent(nullptr); ReadStatus const status = loadLyXFile(); if (status == ReadSuccess) { updateBuffer(); changed(true); updateTitles(); markClean(); message(bformat(_("Document %1$s reloaded."), disp_fn)); d->undo_.clear(); } else { message(bformat(_("Could not reload document %1$s."), disp_fn)); } setBusy(false); removePreviews(); updatePreviews(); errors("Parse"); return status; } bool Buffer::saveAs(FileName const & fn) { FileName const old_name = fileName(); FileName const old_auto = getAutosaveFileName(); bool const old_unnamed = isUnnamed(); bool success = true; d->old_position = filePath(); setFileName(fn); markDirty(); setUnnamed(false); if (save()) { // bring the autosave file with us, just in case. moveAutosaveFile(old_auto); // validate version control data and // correct buffer title lyxvc().file_found_hook(fileName()); updateTitles(); // the file has now been saved to the new location. // we need to check that the locations of child buffers // are still valid. checkChildBuffers(); checkMasterBuffer(); } else { // save failed // reset the old filename and unnamed state setFileName(old_name); setUnnamed(old_unnamed); success = false; } d->old_position.clear(); return success; } void Buffer::checkChildBuffers() { for (auto const & bit : d->children_positions) { DocIterator dit = bit.second; Buffer * cbuf = const_cast(bit.first); if (!cbuf || !theBufferList().isLoaded(cbuf)) continue; Inset * inset = dit.nextInset(); LASSERT(inset && inset->lyxCode() == INCLUDE_CODE, continue); InsetInclude * inset_inc = static_cast(inset); docstring const & incfile = inset_inc->getParam("filename"); string oldloc = cbuf->absFileName(); string newloc = makeAbsPath(to_utf8(incfile), onlyPath(absFileName())).absFileName(); if (oldloc == newloc) continue; // the location of the child file is incorrect. cbuf->setParent(nullptr); inset_inc->setChildBuffer(nullptr); } // invalidate cache of children d->children_positions.clear(); d->position_to_children.clear(); } // If a child has been saved under a different name/path, it might have been // orphaned. Therefore the master needs to be reset (bug 8161). void Buffer::checkMasterBuffer() { Buffer const * const master = masterBuffer(); if (master == this) return; // necessary to re-register the child (bug 5873) // FIXME: clean up updateMacros (here, only // child registering is needed). master->updateMacros(); // (re)set master as master buffer, but only // if we are a real child if (master->isChild(this)) setParent(master); else setParent(nullptr); } string Buffer::includedFilePath(string const & name, string const & ext) const { if (d->old_position.empty() || equivalent(FileName(d->old_position), FileName(filePath()))) return name; bool isabsolute = FileName::isAbsolute(name); // both old_position and filePath() end with a path separator string absname = isabsolute ? name : d->old_position + name; // if old_position is set to origin, we need to do the equivalent of // getReferencedFileName() (see readDocument()) if (!isabsolute && d->old_position == params().origin) { FileName const test(addExtension(filePath() + name, ext)); if (test.exists()) absname = filePath() + name; } if (!FileName(addExtension(absname, ext)).exists()) return name; if (isabsolute) return to_utf8(makeRelPath(from_utf8(name), from_utf8(filePath()))); return to_utf8(makeRelPath(from_utf8(FileName(absname).realPath()), from_utf8(filePath()))); } void Buffer::Impl::refreshFileMonitor() { if (file_monitor_ && file_monitor_->filename() == filename.absFileName()) { file_monitor_->refresh(); return; } // The previous file monitor is invalid // This also destroys the previous file monitor and all its connections file_monitor_ = FileSystemWatcher::monitor(filename); // file_monitor_ will be destroyed with *this, so it is not going to call a // destroyed object method. file_monitor_->connect([this](bool exists) { fileExternallyModified(exists); }); } void Buffer::Impl::fileExternallyModified(bool const exists) { // ignore notifications after our own saving operations if (checksum_ == filename.checksum()) { LYXERR(Debug::FILES, "External modification but " "checksum unchanged: " << filename); return; } lyx_clean = false; // If the file has been deleted, only mark the file as dirty since it is // pointless to prompt for reloading. If later a file is moved into this // location, then the externally modified warning will appear then. if (exists) externally_modified_ = true; // Update external modification notification. // Dirty buffers must be visible at all times. if (wa_ && wa_->unhide(owner_)) { wa_->updateTitles(); if (!exists) { frontend::Alert::warning( _("File deleted from disk"), bformat(_("The file\n %1$s\n" "has been deleted from disk!"), from_utf8(filename.absFileName()))); } } else // Unable to unhide the buffer (e.g. no GUI or not current View) lyx_clean = true; } bool Buffer::notifiesExternalModification() const { return d->externally_modified_; } void Buffer::clearExternalModification() const { d->externally_modified_ = false; if (d->wa_) d->wa_->updateTitles(); } void Buffer::pushIncludedBuffer(Buffer const * buf) const { masterBuffer()->d->include_list_.push_back(buf); if (lyxerr.debugging(Debug::FILES)) { LYXERR0("Pushed. Stack now:"); if (masterBuffer()->d->include_list_.empty()) LYXERR0("EMPTY!"); else for (auto const & b : masterBuffer()->d->include_list_) LYXERR0(b->fileName()); } } void Buffer::popIncludedBuffer() const { masterBuffer()->d->include_list_.pop_back(); if (lyxerr.debugging(Debug::FILES)) { LYXERR0("Popped. Stack now:"); if (masterBuffer()->d->include_list_.empty()) LYXERR0("EMPTY!"); else for (auto const & b : masterBuffer()->d->include_list_) LYXERR0(b->fileName()); } } bool Buffer::isBufferIncluded(Buffer const * buf) const { if (!buf) return false; if (lyxerr.debugging(Debug::FILES)) { LYXERR0("Checking for " << buf->fileName() << ". Stack now:"); if (masterBuffer()->d->include_list_.empty()) LYXERR0("EMPTY!"); else for (auto const & b : masterBuffer()->d->include_list_) LYXERR0(b->fileName()); } list const & blist = masterBuffer()->d->include_list_; return find(blist.begin(), blist.end(), buf) != blist.end(); } void Buffer::clearIncludeList() const { LYXERR(Debug::FILES, "Clearing include list for " << fileName()); d->include_list_.clear(); } } // namespace lyx