[gnome-builder/wip/gtk4-port: 1387/1774] plugins/sphinx-preview: add sphinx preview plugin
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/gtk4-port: 1387/1774] plugins/sphinx-preview: add sphinx preview plugin
- Date: Mon, 11 Jul 2022 22:31:44 +0000 (UTC)
commit 32036081d9d91bd42ed6c57baea21fd00c7406fb
Author: Christian Hergert <chergert redhat com>
Date: Mon Jun 6 21:05:14 2022 -0700
plugins/sphinx-preview: add sphinx preview plugin
There is still more work to be done here for things like rst, but this
gets the harder stuff working which is a sphinx compiler and transforms.
Thanks to IdeHtmlGenerator, we can do this in a much cleaner fashion than
we used to.
meson_options.txt | 1 +
src/plugins/meson.build | 2 +
src/plugins/sphinx-preview/gbp-sphinx-compiler.c | 377 +++++++++++++++++++++
src/plugins/sphinx-preview/gbp-sphinx-compiler.h | 42 +++
.../sphinx-preview/gbp-sphinx-html-generator.c | 248 ++++++++++++++
.../sphinx-preview/gbp-sphinx-html-generator.h | 31 ++
.../gbp-sphinx-preview-workspace-addin.c | 326 ++++++++++++++++++
.../gbp-sphinx-preview-workspace-addin.h | 31 ++
src/plugins/sphinx-preview/gtk/menus.ui | 13 +
src/plugins/sphinx-preview/meson.build | 22 ++
src/plugins/sphinx-preview/sphinx-preview-plugin.c | 48 +++
.../sphinx-preview/sphinx-preview.gresource.xml | 7 +
src/plugins/sphinx-preview/sphinx-preview.plugin | 10 +
13 files changed, 1158 insertions(+)
---
diff --git a/meson_options.txt b/meson_options.txt
index 63d5b049a..77e07973e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -78,6 +78,7 @@ option('plugin_rubocop', type: 'boolean')
option('plugin_rust_analyzer', type: 'boolean')
option('plugin_shellcmd', type: 'boolean')
option('plugin_spellcheck', type: 'boolean')
+option('plugin_sphinx_preview', type: 'boolean')
option('plugin_stylelint', type: 'boolean')
option('plugin_sysprof', type: 'boolean')
option('plugin_sysroot', type: 'boolean')
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 48228513e..27f184111 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -117,6 +117,7 @@ subdir('rust-analyzer')
#subdir('shellcmd')
subdir('snippets')
subdir('spellcheck')
+subdir('sphinx-preview')
subdir('stylelint')
#subdir('sublime')
subdir('support')
@@ -194,6 +195,7 @@ status += [
'Quick Highlight ...................... : @0@'.format(get_option('plugin_quick_highlight')),
'Retab ................................ : @0@'.format(get_option('plugin_retab')),
'rstcheck ............................. : @0@'.format(get_option('plugin_rstcheck')),
+ 'Sphinx Preview (reStructuredText) .... : @0@'.format(get_option('plugin_sphinx_preview')),
'Rubocop .............................. : @0@'.format(get_option('plugin_rubocop')),
'Spellcheck ........................... : @0@'.format(get_option('plugin_spellcheck')),
'Stylelint ............................ : @0@'.format(get_option('plugin_stylelint')),
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-compiler.c
b/src/plugins/sphinx-preview/gbp-sphinx-compiler.c
new file mode 100644
index 000000000..94a7f0143
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-compiler.c
@@ -0,0 +1,377 @@
+/* gbp-sphinx-compiler.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-compiler"
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+
+#include <libide-io.h>
+#include <libide-threading.h>
+
+#include "gbp-sphinx-compiler.h"
+
+struct _GbpSphinxCompiler
+{
+ GObject parent_instance;
+ GFile *config_file;
+ GFile *basedir;
+ GFile *builddir;
+};
+
+G_DEFINE_FINAL_TYPE (GbpSphinxCompiler, gbp_sphinx_compiler, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_CONFIG_FILE,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+GbpSphinxCompiler *
+gbp_sphinx_compiler_new (GFile *config_file)
+{
+ g_return_val_if_fail (G_IS_FILE (config_file), NULL);
+
+ return g_object_new (GBP_TYPE_SPHINX_COMPILER,
+ "config-file", config_file,
+ NULL);
+}
+
+static gboolean
+remove_temporary_directory (GFile *file,
+ GError **error)
+{
+ g_autoptr(IdeDirectoryReaper) reaper = NULL;
+
+ g_assert (G_IS_FILE (file));
+ g_assert (g_file_is_native (file));
+
+ if (!g_file_query_exists (file, NULL))
+ return TRUE;
+
+ reaper = ide_directory_reaper_new ();
+ ide_directory_reaper_add_directory (reaper, file, 0);
+ if (!ide_directory_reaper_execute (reaper, NULL, error))
+ return FALSE;
+
+ if (!g_file_delete (file, NULL, error))
+ return FALSE;
+
+ return TRUE;
+}
+
+static GFile *
+create_temporary_directory (GError **error)
+{
+ g_autofree char *path = NULL;
+
+ if (!(path = g_dir_make_tmp ("gnome-builder-sphinx-XXXXXX", error)))
+ return NULL;
+
+ return g_file_new_for_path (path);
+}
+
+static void
+gbp_sphinx_compiler_constructed (GObject *object)
+{
+ GbpSphinxCompiler *self = (GbpSphinxCompiler *)object;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (GBP_IS_SPHINX_COMPILER (self));
+
+ G_OBJECT_CLASS (gbp_sphinx_compiler_parent_class)->constructed (object);
+
+ if (self->config_file == NULL)
+ g_critical ("%s created without a config-file", G_OBJECT_TYPE_NAME (self));
+ else if (!(self->basedir = g_file_get_parent (self->config_file)))
+ g_critical ("Implausible GFile used as config-file");
+ else if (!(self->builddir = create_temporary_directory (&error)))
+ g_critical ("Failed to create build directory: %s", error->message);
+}
+
+static void
+gbp_sphinx_compiler_finalize (GObject *object)
+{
+ GbpSphinxCompiler *self = (GbpSphinxCompiler *)object;
+
+ if (self->builddir != NULL)
+ {
+ g_autoptr(GError) error = NULL;
+
+ if (!remove_temporary_directory (self->builddir, &error))
+ g_warning ("Failed to cleanup sphinx build directory: %s: %s",
+ g_file_peek_path (self->builddir),
+ error->message);
+
+ g_clear_object (&self->builddir);
+ }
+
+ g_clear_object (&self->config_file);
+ g_clear_object (&self->basedir);
+
+ G_OBJECT_CLASS (gbp_sphinx_compiler_parent_class)->finalize (object);
+}
+
+static void
+gbp_sphinx_compiler_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GbpSphinxCompiler *self = GBP_SPHINX_COMPILER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONFIG_FILE:
+ g_value_set_object (value, self->config_file);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_sphinx_compiler_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GbpSphinxCompiler *self = GBP_SPHINX_COMPILER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONFIG_FILE:
+ self->config_file = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_sphinx_compiler_class_init (GbpSphinxCompilerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = gbp_sphinx_compiler_constructed;
+ object_class->finalize = gbp_sphinx_compiler_finalize;
+ object_class->get_property = gbp_sphinx_compiler_get_property;
+ object_class->set_property = gbp_sphinx_compiler_set_property;
+
+ properties [PROP_CONFIG_FILE] =
+ g_param_spec_object ("config-file",
+ "Config File",
+ "Config File",
+ G_TYPE_FILE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_sphinx_compiler_init (GbpSphinxCompiler *self)
+{
+}
+
+static void
+gbp_sphinx_compiler_load_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GFile *file = (GFile *)object;
+ g_autoptr(IdeTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autofree char *contents = NULL;
+ gsize len;
+
+ IDE_ENTRY;
+
+ g_assert (G_IS_FILE (file));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ if (!g_file_load_contents_finish (file, result, &contents, &len, NULL, &error))
+ ide_task_return_error (task, g_steal_pointer (&error));
+ else
+ ide_task_return_pointer (task, g_steal_pointer (&contents), g_free);
+
+ IDE_EXIT;
+}
+
+static void
+gbp_sphinx_compiler_compile_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeSubprocess *subprocess = (IdeSubprocess *)object;
+ g_autoptr(IdeTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ GCancellable *cancellable;
+ GFile *dest;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_SUBPROCESS (subprocess));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+ {
+ ide_task_return_error (task, g_steal_pointer (&error));
+ IDE_EXIT;
+ }
+
+ dest = ide_task_get_task_data (task);
+ cancellable = ide_task_get_cancellable (task);
+
+ g_assert (G_IS_FILE (dest));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ g_file_load_contents_async (dest,
+ cancellable,
+ gbp_sphinx_compiler_load_cb,
+ g_steal_pointer (&task));
+
+ IDE_EXIT;
+}
+
+static char *
+replace_suffix (const char *str,
+ const char *suffix)
+{
+ const char *dot = strrchr (str, '.');
+ char *shortened;
+ char *ret;
+
+ if (dot == NULL)
+ return g_strdup (str);
+
+ shortened = g_strndup (str, dot - str);
+ ret = g_strconcat (shortened, suffix, NULL);
+ g_free (shortened);
+
+ return ret;
+}
+
+static void
+gbp_sphinx_compiler_purge_doctree (GbpSphinxCompiler *self,
+ const char *relpath)
+{
+ g_autofree char *doctreepath = NULL;
+ g_autofree char *fullpath = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_SPHINX_COMPILER (self));
+ g_assert (relpath != NULL);
+
+ doctreepath = replace_suffix (relpath, ".doctree");
+ fullpath = g_build_filename (g_file_peek_path (self->builddir),
+ ".doctrees",
+ doctreepath,
+ NULL);
+
+ g_unlink (fullpath);
+
+ IDE_EXIT;
+}
+
+void
+gbp_sphinx_compiler_compile_async (GbpSphinxCompiler *self,
+ GFile *file,
+ const char *contents,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+ g_autoptr(IdeSubprocess) subprocess = NULL;
+ g_autoptr(IdeTask) task = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) tu = NULL;
+ g_autofree char *relpath = NULL;
+ g_autofree char *htmlpath = NULL;
+ const char *path;
+
+ IDE_ENTRY;
+
+ g_return_if_fail (GBP_IS_SPHINX_COMPILER (self));
+ g_return_if_fail (G_IS_FILE (file));
+ g_return_if_fail (g_file_has_prefix (file, self->basedir));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = ide_task_new (self, cancellable, callback, user_data);
+ ide_task_set_source_tag (task, gbp_sphinx_compiler_compile_async);
+
+ if (!g_file_is_native (file))
+ {
+ ide_task_return_unsupported_error (task);
+ IDE_EXIT;
+ }
+
+ path = g_file_peek_path (file);
+ relpath = g_file_get_relative_path (self->basedir, file);
+ htmlpath = replace_suffix (relpath, ".html");
+ tu = g_file_get_child (self->builddir, htmlpath);
+ ide_task_set_task_data (task, g_steal_pointer (&tu), g_object_unref);
+
+ gbp_sphinx_compiler_purge_doctree (self, relpath);
+
+ g_assert (path != NULL);
+ g_assert (G_IS_FILE (self->basedir));
+ g_assert (G_IS_FILE (self->builddir));
+ g_assert (g_file_peek_path (self->basedir) != NULL);
+ g_assert (g_file_peek_path (self->builddir) != NULL);
+
+ launcher = ide_subprocess_launcher_new (0);
+ ide_subprocess_launcher_push_args (launcher, IDE_STRV_INIT ("sphinx-build", "-Q", "-b", "html"));
+ ide_subprocess_launcher_push_argv (launcher, g_file_peek_path (self->basedir));
+ ide_subprocess_launcher_push_argv (launcher, g_file_peek_path (self->builddir));
+ ide_subprocess_launcher_push_argv (launcher, path);
+
+ if (!(subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error)))
+ {
+ ide_task_return_error (task, g_steal_pointer (&error));
+ IDE_EXIT;
+ }
+
+ ide_subprocess_wait_check_async (subprocess,
+ cancellable,
+ gbp_sphinx_compiler_compile_cb,
+ g_steal_pointer (&task));
+
+ IDE_EXIT;
+}
+
+char *
+gbp_sphinx_compiler_compile_finish (GbpSphinxCompiler *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GBP_IS_SPHINX_COMPILER (self), NULL);
+ g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+ return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-compiler.h
b/src/plugins/sphinx-preview/gbp-sphinx-compiler.h
new file mode 100644
index 000000000..7826bd636
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-compiler.h
@@ -0,0 +1,42 @@
+/* gbp-sphinx-compiler.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_COMPILER (gbp_sphinx_compiler_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxCompiler, gbp_sphinx_compiler, GBP, SPHINX_COMPILER, GObject)
+
+GbpSphinxCompiler *gbp_sphinx_compiler_new (GFile *config_file);
+void gbp_sphinx_compiler_compile_async (GbpSphinxCompiler *self,
+ GFile *file,
+ const char *contents,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+char *gbp_sphinx_compiler_compile_finish (GbpSphinxCompiler *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c
b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c
new file mode 100644
index 000000000..d02ac6e98
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c
@@ -0,0 +1,248 @@
+/* gbp-sphinx-html-generator.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-html-generator"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "gbp-sphinx-compiler.h"
+#include "gbp-sphinx-html-generator.h"
+
+struct _GbpSphinxHtmlGenerator
+{
+ IdeHtmlGenerator parent_instance;
+ GSignalGroup *buffer_signals;
+ GbpSphinxCompiler *compiler;
+};
+
+enum {
+ PROP_0,
+ PROP_BUFFER,
+ PROP_COMPILER,
+ N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (GbpSphinxHtmlGenerator, gbp_sphinx_html_generator, IDE_TYPE_HTML_GENERATOR)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_sphinx_html_generator_compile_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GbpSphinxCompiler *compiler = (GbpSphinxCompiler *)object;
+ g_autoptr(IdeTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ g_autofree char *contents = NULL;
+ gsize len;
+
+ g_assert (GBP_IS_SPHINX_COMPILER (compiler));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ if (!(contents = gbp_sphinx_compiler_compile_finish (compiler, result, &error)))
+ {
+ ide_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ len = strlen (contents);
+ ide_task_return_pointer (task,
+ g_bytes_new_take (g_steal_pointer (&contents), len),
+ g_bytes_unref);
+}
+
+static void
+gbp_sphinx_html_generator_generate_async (IdeHtmlGenerator *generator,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GbpSphinxHtmlGenerator *self = (GbpSphinxHtmlGenerator *)generator;
+ g_autoptr(IdeBuffer) buffer = NULL;
+ g_autoptr(IdeTask) task = NULL;
+ GFile *file;
+
+ g_assert (GBP_IS_SPHINX_HTML_GENERATOR (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = ide_task_new (self, cancellable, callback, user_data);
+ ide_task_set_source_tag (task, gbp_sphinx_html_generator_generate_async);
+
+ if (self->compiler == NULL ||
+ !(buffer = g_signal_group_dup_target (self->buffer_signals)))
+ {
+ ide_task_return_new_error (task,
+ G_IO_ERROR,
+ G_IO_ERROR_CANCELLED,
+ "Operation was cancelled");
+ return;
+ }
+
+ file = ide_buffer_get_file (buffer);
+
+ gbp_sphinx_compiler_compile_async (self->compiler,
+ file,
+ NULL,
+ cancellable,
+ gbp_sphinx_html_generator_compile_cb,
+ g_steal_pointer (&task));
+}
+
+static GBytes *
+gbp_sphinx_html_generator_generate_finish (IdeHtmlGenerator *generator,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (GBP_IS_SPHINX_HTML_GENERATOR (generator));
+ g_assert (IDE_IS_TASK (result));
+
+ return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static gboolean
+file_to_base_uri (GBinding *binding,
+ const GValue *from,
+ GValue *to,
+ gpointer user_data)
+{
+ g_value_set_string (to, g_file_get_uri (g_value_get_object (from)));
+ return TRUE;
+}
+
+static void
+gbp_sphinx_html_generator_set_buffer (GbpSphinxHtmlGenerator *self,
+ IdeBuffer *buffer)
+{
+ g_assert (GBP_IS_SPHINX_HTML_GENERATOR (self));
+ g_assert (!buffer || IDE_IS_BUFFER (buffer));
+
+ g_signal_group_set_target (self->buffer_signals, buffer);
+
+ if (IDE_IS_BUFFER (buffer))
+ g_object_bind_property_full (buffer, "file",
+ self, "base-uri",
+ G_BINDING_SYNC_CREATE,
+ file_to_base_uri,
+ NULL, NULL, NULL);
+}
+
+static void
+gbp_sphinx_html_generator_dispose (GObject *object)
+{
+ GbpSphinxHtmlGenerator *self = (GbpSphinxHtmlGenerator *)object;
+
+ g_clear_object (&self->buffer_signals);
+ g_clear_object (&self->compiler);
+
+ G_OBJECT_CLASS (gbp_sphinx_html_generator_parent_class)->dispose (object);
+}
+
+static void
+gbp_sphinx_html_generator_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GbpSphinxHtmlGenerator *self = GBP_SPHINX_HTML_GENERATOR (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_take_object (value, g_signal_group_dup_target (self->buffer_signals));
+ break;
+
+ case PROP_COMPILER:
+ g_value_set_object (value, self->compiler);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_sphinx_html_generator_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GbpSphinxHtmlGenerator *self = GBP_SPHINX_HTML_GENERATOR (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ gbp_sphinx_html_generator_set_buffer (self, g_value_get_object (value));
+ break;
+
+ case PROP_COMPILER:
+ self->compiler = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_sphinx_html_generator_class_init (GbpSphinxHtmlGeneratorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ IdeHtmlGeneratorClass *generator_class = IDE_HTML_GENERATOR_CLASS (klass);
+
+ object_class->dispose = gbp_sphinx_html_generator_dispose;
+ object_class->get_property = gbp_sphinx_html_generator_get_property;
+ object_class->set_property = gbp_sphinx_html_generator_set_property;
+
+ generator_class->generate_async = gbp_sphinx_html_generator_generate_async;
+ generator_class->generate_finish = gbp_sphinx_html_generator_generate_finish;
+
+ properties [PROP_BUFFER] =
+ g_param_spec_object ("buffer", NULL, NULL,
+ IDE_TYPE_BUFFER,
+ (G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_COMPILER] =
+ g_param_spec_object ("compiler", NULL, NULL,
+ GBP_TYPE_SPHINX_COMPILER,
+ (G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_sphinx_html_generator_init (GbpSphinxHtmlGenerator *self)
+{
+ self->buffer_signals = g_signal_group_new (IDE_TYPE_BUFFER);
+
+ g_signal_group_connect_object (self->buffer_signals,
+ "changed",
+ G_CALLBACK (ide_html_generator_invalidate),
+ self,
+ G_CONNECT_SWAPPED);
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h
b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h
new file mode 100644
index 000000000..a52de2268
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h
@@ -0,0 +1,31 @@
+/* gbp-sphinx-html-generator.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-webkit.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_HTML_GENERATOR (gbp_sphinx_html_generator_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxHtmlGenerator, gbp_sphinx_html_generator, GBP, SPHINX_HTML_GENERATOR,
IdeHtmlGenerator)
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c
b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c
new file mode 100644
index 000000000..803df3208
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c
@@ -0,0 +1,326 @@
+/* gbp-sphinx-preview-workspace-addin.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-preview-workspace-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-webkit.h>
+
+#include "gbp-sphinx-compiler.h"
+#include "gbp-sphinx-html-generator.h"
+#include "gbp-sphinx-preview-workspace-addin.h"
+
+struct _GbpSphinxPreviewWorkspaceAddin
+{
+ GObject parent_instance;
+ IdeWorkspace *workspace;
+ GSignalGroup *buffer_signals;
+ IdeEditorPage *editor_page;
+ GHashTable *compilers;
+};
+
+static void live_preview_action (GbpSphinxPreviewWorkspaceAddin *self,
+ GVariant *params);
+
+IDE_DEFINE_ACTION_GROUP (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin, {
+ { "sphinx-preview", live_preview_action },
+})
+
+static void
+gbp_sphinx_preview_workspace_addin_set_language (GbpSphinxPreviewWorkspaceAddin *self,
+ const char *language_id)
+{
+ gboolean enabled;
+
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+
+ IDE_TRACE_MSG ("Switching language-id to %s", language_id ? language_id : "NULL");
+
+ enabled = ide_str_equal0 (language_id, "rst");
+ gbp_sphinx_preview_workspace_addin_set_action_enabled (self, "sphinx-preview", enabled);
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_page_changed (IdeWorkspaceAddin *addin,
+ IdePage *page)
+{
+ GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+ IdeBuffer *buffer = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (!page || IDE_IS_PAGE (page));
+
+ self->editor_page = NULL;
+
+ /* Make sure the page is an editor page and a local file, as that
+ * is the only kind of files we can process with sphinx.
+ */
+ if (IDE_IS_EDITOR_PAGE (page))
+ {
+ GFile *file = ide_editor_page_get_file (IDE_EDITOR_PAGE (page));
+
+ if (g_file_is_native (file))
+ {
+ self->editor_page = IDE_EDITOR_PAGE (page);
+ buffer = ide_editor_page_get_buffer (self->editor_page);
+ }
+ }
+
+ g_signal_group_set_target (self->buffer_signals, buffer);
+
+ IDE_EXIT;
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_notify_language_id (GbpSphinxPreviewWorkspaceAddin *self,
+ GParamSpec *pspec,
+ IdeBuffer *buffer)
+{
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_BUFFER (buffer));
+
+ gbp_sphinx_preview_workspace_addin_set_language (self, ide_buffer_get_language_id (buffer));
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_bind (GbpSphinxPreviewWorkspaceAddin *self,
+ IdeBuffer *buffer,
+ GSignalGroup *signal_group)
+{
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_BUFFER (buffer));
+ g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+ gbp_sphinx_preview_workspace_addin_set_language (self, ide_buffer_get_language_id (buffer));
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_unbind (GbpSphinxPreviewWorkspaceAddin *self,
+ GSignalGroup *signal_group)
+{
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+ gbp_sphinx_preview_workspace_addin_set_language (self, NULL);
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_load (IdeWorkspaceAddin *addin,
+ IdeWorkspace *workspace)
+{
+ GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_WORKSPACE (workspace));
+
+ self->workspace = workspace;
+
+ self->compilers = g_hash_table_new_full (g_file_hash,
+ (GEqualFunc)g_file_equal,
+ g_object_unref,
+ g_object_unref);
+
+ self->buffer_signals = g_signal_group_new (IDE_TYPE_BUFFER);
+ g_signal_connect_object (self->buffer_signals,
+ "bind",
+ G_CALLBACK (gbp_sphinx_preview_workspace_addin_bind),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->buffer_signals,
+ "unbind",
+ G_CALLBACK (gbp_sphinx_preview_workspace_addin_unbind),
+ self,
+ G_CONNECT_SWAPPED);
+ g_signal_group_connect_object (self->buffer_signals,
+ "notify::language-id",
+ G_CALLBACK (gbp_sphinx_preview_workspace_addin_notify_language_id),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+ "sphinx-preview",
+ G_ACTION_GROUP (self));
+
+ IDE_EXIT;
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_unload (IdeWorkspaceAddin *addin,
+ IdeWorkspace *workspace)
+{
+ GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_WORKSPACE (workspace));
+
+ gtk_widget_insert_action_group (GTK_WIDGET (workspace), "sphinx-preview", NULL);
+
+ g_clear_pointer (&self->compilers, g_hash_table_unref);
+ g_clear_object (&self->buffer_signals);
+
+ self->editor_page = NULL;
+ self->workspace = NULL;
+
+ IDE_EXIT;
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+ iface->load = gbp_sphinx_preview_workspace_addin_load;
+ iface->unload = gbp_sphinx_preview_workspace_addin_unload;
+ iface->page_changed = gbp_sphinx_preview_workspace_addin_page_changed;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin,
G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init)
+ G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
gbp_sphinx_preview_workspace_addin_init_action_group))
+
+static void
+gbp_sphinx_preview_workspace_addin_class_init (GbpSphinxPreviewWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_init (GbpSphinxPreviewWorkspaceAddin *self)
+{
+ gbp_sphinx_preview_workspace_addin_set_action_enabled (self, "sphinx-preview", FALSE);
+}
+
+static IdePage *
+open_sphinx_preview (GbpSphinxPreviewWorkspaceAddin *self,
+ IdeBuffer *buffer,
+ GFile *conf_py)
+{
+ g_autoptr(IdeHtmlGenerator) generator = NULL;
+ GbpSphinxCompiler *compiler;
+ IdeWebkitPage *page;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_BUFFER (buffer));
+ g_assert (G_IS_FILE (conf_py));
+ g_assert (self->compilers != NULL);
+
+ if (!(compiler = g_hash_table_lookup (self->compilers, conf_py)))
+ {
+ compiler = gbp_sphinx_compiler_new (conf_py);
+ g_hash_table_insert (self->compilers, g_file_dup (conf_py), compiler);
+ }
+
+ generator = g_object_new (GBP_TYPE_SPHINX_HTML_GENERATOR,
+ "buffer", buffer,
+ "compiler", compiler,
+ NULL);
+ page = ide_webkit_page_new_for_generator (generator);
+
+ IDE_RETURN (IDE_PAGE (page));
+}
+
+static IdePage *
+open_rst_preview (GbpSphinxPreviewWorkspaceAddin *self,
+ IdeBuffer *buffer)
+{
+ g_autoptr(IdeHtmlGenerator) generator = NULL;
+ IdeWebkitPage *page;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_BUFFER (buffer));
+
+ /* TODO: docutils translation with python in subprocess */
+ generator = ide_html_generator_new_for_buffer (GTK_TEXT_BUFFER (buffer));
+ page = ide_webkit_page_new_for_generator (generator);
+
+ IDE_RETURN (IDE_PAGE (page));
+}
+
+static void
+live_preview_action (GbpSphinxPreviewWorkspaceAddin *self,
+ GVariant *params)
+{
+ g_autoptr(IdePanelPosition) position = NULL;
+ g_autoptr(IdeBuffer) buffer = NULL;
+ g_autoptr(GFile) workdir = NULL;
+ g_autoptr(GFile) parent = NULL;
+ IdeContext *context;
+ IdePage *page = NULL;
+ GFile *file;
+ guint column;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+ g_assert (IDE_IS_WORKSPACE (self->workspace));
+ g_assert (IDE_IS_EDITOR_PAGE (self->editor_page));
+
+ context = ide_workspace_get_context (self->workspace);
+ workdir = ide_context_ref_workdir (context);
+ file = ide_editor_page_get_file (self->editor_page);
+ parent = g_file_get_parent (file);
+ buffer = g_signal_group_dup_target (self->buffer_signals);
+ position = ide_page_get_position (IDE_PAGE (self->editor_page));
+
+ if (!ide_panel_position_get_column (position, &column))
+ column = 0;
+
+ ide_panel_position_set_column (position, column + 1);
+ ide_panel_position_set_depth (position, 0);
+
+ while (parent != NULL &&
+ (g_file_equal (workdir, parent) || g_file_has_prefix (parent, workdir)))
+ {
+ g_autoptr(GFile) conf_py = g_file_get_child (parent, "conf.py");
+ g_autoptr(GFile) old_parent = NULL;
+
+ if (g_file_query_exists (conf_py, NULL))
+ {
+ /* Found our top-level sphinx directory */
+ page = open_sphinx_preview (self, buffer, conf_py);
+ break;
+ }
+
+ old_parent = parent;
+ parent = g_file_get_parent (old_parent);
+ }
+
+ if (page == NULL)
+ page = open_rst_preview (self, buffer);
+
+ ide_workspace_add_page (self->workspace, page, position);
+ panel_widget_raise (PANEL_WIDGET (page));
+
+ IDE_EXIT;
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h
b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h
new file mode 100644
index 000000000..44f46e203
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-sphinx-preview-workspace-addin.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_PREVIEW_WORKSPACE_ADDIN (gbp_sphinx_preview_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin, GBP,
SPHINX_PREVIEW_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gtk/menus.ui b/src/plugins/sphinx-preview/gtk/menus.ui
new file mode 100644
index 000000000..1dc905380
--- /dev/null
+++ b/src/plugins/sphinx-preview/gtk/menus.ui
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interface>
+ <menu id="ide-editor-page-menu">
+ <section id="ide-editor-page-preview-section">
+ <item>
+ <attribute name="id">sphinx-preview-item</attribute>
+ <attribute name="label" translatable="yes">Open Preview…</attribute>
+ <attribute name="action">sphinx-preview.sphinx-preview</attribute>
+ <attribute name="hidden-when">action-disabled</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/src/plugins/sphinx-preview/meson.build b/src/plugins/sphinx-preview/meson.build
new file mode 100644
index 000000000..ccd02f713
--- /dev/null
+++ b/src/plugins/sphinx-preview/meson.build
@@ -0,0 +1,22 @@
+if get_option('plugin_sphinx_preview')
+
+if not get_option('webkit').enabled()
+ error('-Dwebkit=enabled is required for sphinx-preview plugin')
+endif
+
+plugins_sources += files([
+ 'sphinx-preview-plugin.c',
+ 'gbp-sphinx-compiler.c',
+ 'gbp-sphinx-html-generator.c',
+ 'gbp-sphinx-preview-workspace-addin.c',
+])
+
+plugin_sphinx_preview_resources = gnome.compile_resources(
+ 'sphinx-preview-resources',
+ 'sphinx-preview.gresource.xml',
+ c_name: 'gbp_sphinx_preview'
+)
+
+plugins_sources += plugin_sphinx_preview_resources
+
+endif
diff --git a/src/plugins/sphinx-preview/sphinx-preview-plugin.c
b/src/plugins/sphinx-preview/sphinx-preview-plugin.c
new file mode 100644
index 000000000..858b0d7be
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview-plugin.c
@@ -0,0 +1,48 @@
+/* sphinx-preview-plugin.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sphinx-preview-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+
+#include <libide-gui.h>
+
+#include "gbp-sphinx-preview-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_sphinx_preview_register_types (PeasObjectModule *module)
+{
+ const char *path;
+
+ if (!(path = g_find_program_in_path ("sphinx-build")))
+ {
+ /* We always have it in the Flatpak, so just complain for other types
+ * of installations which are incomplete.
+ */
+ g_debug ("sphinx-build not found in PATH. Refusing to register addins.");
+ return;
+ }
+
+ peas_object_module_register_extension_type (module,
+ IDE_TYPE_WORKSPACE_ADDIN,
+ GBP_TYPE_SPHINX_PREVIEW_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/sphinx-preview/sphinx-preview.gresource.xml
b/src/plugins/sphinx-preview/sphinx-preview.gresource.xml
new file mode 100644
index 000000000..cb615b98c
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/plugins/sphinx-preview">
+ <file>sphinx-preview.plugin</file>
+ <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/plugins/sphinx-preview/sphinx-preview.plugin
b/src/plugins/sphinx-preview/sphinx-preview.plugin
new file mode 100644
index 000000000..40e5f4233
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2022 Christian Hergert
+Depends=editorui;webkit;
+Description=Live preview of HTML documents
+Embedded=_gbp_sphinx_preview_register_types
+Module=sphinx-preview
+Name=HTML Preview
+X-Workspace-Kind=primary;editor;
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]