#!/usr/bin/env python3 # # html_preview_plugin.py # # Copyright (C) 2015 Christian Hergert # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import builtins import gi import io import locale import os import shutil import sys import subprocess import threading gi.require_version('Gtk', '3.0') gi.require_version('Ide', '1.0') gi.require_version('WebKit2', '4.0') from gi.repository import GLib from gi.repository import Gio from gi.repository import Gtk from gi.repository import GObject from gi.repository import Ide from gi.repository import WebKit2 from gi.repository import Peas try: locale.setlocale(locale.LC_ALL, '') except: pass can_preview_rst = True can_preview_sphinx = True old_open = None try: from docutils.core import publish_string except ImportError: can_preview_rst = False try: import sphinx except ImportError: can_preview_sphinx = False sphinx_states = {} sphinx_override = {} def add_override_file(path, content): if path in sphinx_override: return False else: sphinx_override[path] = content.encode('utf-8') return True def remove_override_file(path): try: del sphinx_override[path] except KeyError: return False return True def new_open(*args, **kwargs): path = args[0] if path in sphinx_override: return io.BytesIO(sphinx_override[path]) return old_open(*args, **kwargs) old_open = builtins.open builtins.open = new_open _ = Ide.gettext def is_sphinx_installed(): with open(os.devnull, 'w') as devnull: try: if subprocess.call(['sphinx-build', '--version'], stdout=devnull, stderr=devnull) == 0: return True except FileNotFoundError: pass return False class SphinxState(): def __init__(self, builddir): self.builddir = builddir self.is_running = False self.need_build = False class HtmlPreviewData(GObject.Object, Ide.ApplicationAddin): MARKDOWN_CSS = None MARKED_JS = None MARKDOWN_VIEW_JS = None def do_load(self, app): HtmlPreviewData.MARKDOWN_CSS = self.get_data('markdown.css') HtmlPreviewData.MARKED_JS = self.get_data('marked.js') HtmlPreviewData.MARKDOWN_VIEW_JS = self.get_data('markdown-view.js') def do_unload(self, app): for state in sphinx_states.items(): # Be extra sure that we are in the tmp dir tmpdir = GLib.get_tmp_dir() if state.builddir.startswith(tmpdir): shutil.rmtree(state.builddir) def get_data(self, name): engine = Peas.Engine.get_default() info = engine.get_plugin_info('html_preview_plugin') datadir = info.get_data_dir() path = os.path.join(datadir, name) return open(path, 'r').read() class HtmlWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin): def do_load(self, workbench): self.workbench = workbench group = Gio.SimpleActionGroup() self.install_action = Gio.SimpleAction(name='install-docutils', enabled=True) self.install_action.connect('activate', lambda *_: self.install_docutils()) group.insert(self.install_action) self.install_action = Gio.SimpleAction(name='install-sphinx', enabled=True) self.install_action.connect('activate', lambda *_: self.install_sphinx()) group.insert(self.install_action) self.workbench.insert_action_group('html-preview', group) def do_unload(self, workbench): self.workbench = None def install_docutils(self): transfer = Ide.PkconTransfer(packages=['python3-docutils']) context = self.workbench.get_context() manager = context.get_transfer_manager() manager.execute_async(transfer, None, self.docutils_installed, None) def install_sphinx(self): transfer = Ide.PkconTransfer(packages=['python3-sphinx']) context = self.workbench.get_context() manager = context.get_transfer_manager() manager.execute_async(transfer, None, self.sphinx_installed, None) def docutils_installed(self, object, result, data): global can_preview_rst global publish_string try: from docutils.core import publish_string except ImportError: return can_preview_rst = True self.workbench.pop_message('org.gnome.builder.docutils.install') def sphinx_installed(self, object, result, data): global can_preview_sphinx global sphinx try: import sphinx except ImportError: return can_preview_sphinx = True self.workbench.pop_message('org.gnome.builder.sphinx.install') class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin): def do_load(self, view): self.workbench = view.get_ancestor(Ide.Workbench) self.view = view self.can_preview = False self.sphinx_basedir = None self.sphinx_builddir = None self.action = Gio.SimpleAction(name='preview-as-html', enabled=True) self.action.connect('activate', lambda *_: self.preview_activated(view)) actions = view.get_action_group('view') actions.add_action(self.action) document = view.get_document() language = document.get_language() language_id = language.get_id() if language else None self.do_language_changed(language_id) def do_unload(self, view): actions = view.get_action_group('view') actions.remove_action('preview-as-html') def do_language_changed(self, language_id): enabled = (language_id in ('html', 'markdown', 'rst')) self.action.set_enabled(enabled) self.lang_id = language_id self.can_preview = enabled if self.lang_id == 'rst': if not self.sphinx_basedir: document = self.view.get_document() path = document.get_file().get_file().get_path() self.sphinx_basedir = self.search_sphinx_base_dir(path) if self.sphinx_basedir: self.sphinx_builddir = self.setup_sphinx_states(self.sphinx_basedir) if not enabled: self.sphinx_basedir = None self.sphinx_builddir = None def setup_sphinx_states(self, basedir): global sphinx_states if basedir in sphinx_states: state = sphinx_states[basedir] sphinx_builddir = state.builddir else: sphinx_builddir = GLib.Dir.make_tmp('gnome-builder-sphinx-build-XXXXXX') state = SphinxState(sphinx_builddir) sphinx_states[basedir] = state return sphinx_builddir def preview_activated(self, view): global can_preview_rst if self.lang_id == 'rst': if self.sphinx_basedir: if not can_preview_sphinx: self.show_missing_sphinx_message(view) return elif not can_preview_rst: self.show_missing_docutils_message(view) return document = view.get_document() web_view = HtmlPreviewView(document, self.sphinx_basedir, self.sphinx_builddir, visible=True) stack = view.get_ancestor(Ide.LayoutStack) stack.add(web_view) self.action.set_enabled(False) web_view.connect('destroy', lambda *_: self.web_view_destroyed(web_view)) def web_view_destroyed(self, web_view): self.action.set_enabled(True) def search_sphinx_base_dir(self, path): context = self.workbench.get_context() vcs = context.get_vcs() working_dir = vcs.get_working_directory().get_path() try: if os.path.commonpath([working_dir, path]) != working_dir: working_dir = '/' except: working_dir = '/' folder = os.path.dirname(path) level = 10 while level > 0: files = os.scandir(folder) for file in files: if file.name == 'conf.py': return folder if folder == working_dir: return None level -= 1 folder = os.path.dirname(folder) def show_missing_docutils_message(self, view): message = Ide.WorkbenchMessage( id='org.gnome.builder.docutils.install', title=_('Your computer is missing python3-docutils'), show_close_button=True, visible=True) message.add_action(_('Install'), 'html-preview.install-docutils') self.workbench.push_message(message) def show_missing_sphinx_message(self, view): message = Ide.WorkbenchMessage( id='org.gnome.builder.sphinx.install', title=_('Your computer is missing python3-sphinx'), show_close_button=True, visible=True) message.add_action(_('Install'), 'html-preview.install-sphinx') self.workbench.push_message(message) class HtmlPreviewView(Ide.LayoutView): markdown = False rst = False def __init__(self, document, sphinx_basedir, sphinx_builddir, *args, **kwargs): global old_open super().__init__(*args, **kwargs) self.sphinx_basedir = sphinx_basedir self.sphinx_builddir = sphinx_builddir self.document = document self.webview = WebKit2.WebView(visible=True, expand=True) self.add(self.webview) settings = self.webview.get_settings() settings.enable_html5_local_storage = False language = document.get_language() if language: id = language.get_id() if id == 'markdown': self.markdown = True elif id == 'rst': self.rst = True document.connect('changed', self.on_changed) self.on_changed(document) def do_get_title(self): title = self.document.get_title() return '%s (Preview)' % title def do_get_document(self): return self.document def get_markdown(self, text): text = text.replace("\"", "\\\"").replace("\n", "\\n") params = (HtmlPreviewData.MARKDOWN_CSS, text, HtmlPreviewData.MARKED_JS, HtmlPreviewData.MARKDOWN_VIEW_JS) return """
""" % params def get_rst(self, text, path): return publish_string(text, writer_name='html5', source_path=path, destination_path=path) def get_sphinx_rst_async(self, text, path, basedir, builddir, cancellable, callback): task = Gio.Task.new(self, cancellable, callback) threading.Thread(target=self.get_sphinx_rst_worker, args=[task, text, path, basedir, builddir], name='sphinx-rst-thread').start() def purge_cache(self, basedir, builddir, document): path = document.get_file().get_file().get_path() rel_path = os.path.relpath(path, start=basedir) rel_path_doctree = os.path.splitext(rel_path)[0] + '.doctree' doctree_path = os.path.join(builddir, '.doctrees', rel_path_doctree) tmpdir = GLib.get_tmp_dir() if doctree_path.startswith(tmpdir): try: os.remove(doctree_path) except: pass def get_sphinx_rst_worker(self, task, text, path, basedir, builddir): add_override_file(path, text) rel_path = os.path.relpath(path, start=basedir) command = ['sphinx-build', '-Q', '-b', 'html', basedir, builddir, path] rel_path_html = os.path.splitext(rel_path)[0] + '.html' builddir_path = os.path.join(builddir, rel_path_html) result = not sphinx.build_main(command) remove_override_file(path) if not result: task.builddir_path = None task.return_error(GLib.Error('\'sphinx-build\' command error for {}'.format(path))) return task.builddir_path = builddir_path task.return_boolean(True) def get_sphinx_rst_finish(self, result): succes = result.propagate_boolean() builddir_path = result.builddir_path return builddir_path def get_sphinx_state(self, basedir): global sphinx_states try: state = sphinx_states[basedir] except KeyError: return None return state def reload(self): state = self.get_sphinx_state(self.sphinx_basedir) if state and state.is_running: state.need_build = True return gfile = self.document.get_file().get_file() base_uri = gfile.get_uri() begin, end = self.document.get_bounds() text = self.document.get_text(begin, end, True) if self.markdown: text = self.get_markdown(text) elif self.rst: if self.sphinx_basedir: self.purge_cache(self.sphinx_basedir, self.sphinx_builddir, self.document) state.is_running = True self.get_sphinx_rst_async(text, gfile.get_path(), self.sphinx_basedir, self.sphinx_builddir, None, self.get_sphinx_rst_cb) return else: text = self.get_rst(text, gfile.get_path()).decode("utf-8") self.webview.load_html(text, base_uri) def get_sphinx_rst_cb(self, obj, result): builddir_path = self.get_sphinx_rst_finish(result) if builddir_path: uri = 'file:///' + builddir_path self.webview.load_uri(uri) state = self.get_sphinx_state(self.sphinx_basedir) state.is_running = False if state.need_build: state.need_build = False self.reload() def on_changed(self, document): self.reload()