#!/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()