13 Commits
Author SHA1 Message Date
NotAlexNoyleandClaude Opus 4.8 181cbf79b2 Add GitHub-style syntax highlighting to the code-block preview
The WebKit HTML preview previously rendered fenced code blocks
monochrome. Wire the existing tokenizer into the HTML renderer so code
blocks are highlighted like GitHub, and extend language coverage.

- markdown.c: accumulate each code block's text, tokenize it on block
  leave, and emit <span class="pl-*"> per token with HTML-escaped gaps
  and token text; unknown languages fall back to plain escaped text.
- Add GitHub Primer syntax colors (light/dark) for keywords, constants,
  strings, and comments.
- code_highlight.c: tokenize comments (//, /* */, #), which were
  previously skipped and left uncolored.
- Add languages: javascript, kotlin, and bash (aliases js, kt, sh,
  shell). Bash uses a dedicated scanner whose '#' comments are
  word-position aware so $#, x=$#, and a.com#frag are not comments.
- Create the code_comment tag so the source editor colors comments too.
- README: document the highlighted languages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:20:50 -04:00
NotAlexNoyleandClaude Opus 4.8 66cf21ec71 README: drop distro package install and packaging templates sections
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:59:19 -04:00
NotAlexNoyleandClaude Opus 4.8 f28b5a8145 Fix packaging CI and polish window UI
- CI: install WebKit dev packages in the deb/rpm jobs so the build links
  against webkit2gtk-4.1 (previously only GTK headers were installed, so
  compilation would have failed). Verified both jobs end-to-end in
  ubuntu:22.04 and fedora:40 containers.
- Server-side window decorations: replace the CSD GtkHeaderBar with a
  plain in-window toolbar so the window manager (e.g. KDE/KWin) draws
  full window controls.
- About popover: link the SeeMD title to the repo and the ViewMD mention
  to upstream; fix the "fork of SeeMD" text back to ViewMD.
- Replace the About text button with an eye-glyph icon button.
- Focus the document on startup instead of the Open toolbar button.
- README: use the assets/seemd.png screenshot and add an eye glyph to the
  title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:55:13 -04:00
NotAlexNoyle 5c4affefbd SeeMD rebrand 2026-07-01 14:33:04 -04:00
NotAlexNoyle e27c392078 ViewMD -> SeeMD 2026-07-01 14:24:11 -04:00
rabfulton effa685762 Merge pull request #1 from FenrirTheGray/fix/reload-file-crash
Fix error when reloading file more than once
2026-04-10 11:28:26 +01:00
Aleksandar Colovic a30ce043f4 Fix crash when reloading file more than once
markyd_app_open_file() invalidates the current path string, so calling
it with the pointer from markyd_app_get_current_path() works the first
time but causes a use-after-free on subsequent reloads. Copy the path
with g_strdup() before passing it to open_file, and free it afterwards.
2026-04-08 18:46:24 +02:00
Robert Fulton 3d36df9847 Update README.md 2026-02-26 02:09:07 +00:00
Robert Fulton 0cce704eec Support hanging indents for list items 2026-02-26 02:06:58 +00:00
Robert Fulton 55eff8c494 Fix for search within tables 2026-02-26 01:50:11 +00:00
Robert Fulton c40a410d7d Add support for local images 2026-02-26 01:18:04 +00:00
Robert Fulton ab0b9b37d7 Add search on document 2026-02-23 17:52:51 +00:00
Robert Fulton 9bfb95e5a7 Update README.md 2026-02-19 11:30:50 +00:00
30 changed files with 4699 additions and 230 deletions

No files matched your search

+15 -15
View File
@@ -21,7 +21,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential pkg-config \
libgtk-3-dev
libgtk-3-dev libwebkit2gtk-4.1-dev
- name: Build
run: make -j
@@ -29,8 +29,8 @@ jobs:
- name: ABI check (GLib)
run: |
set -euo pipefail
if nm -D viewmd | grep -q ' g_string_free_and_steal$'; then
echo "Error: viewmd links against g_string_free_and_steal (too-new GLib ABI for jammy/mint 21.x)" >&2
if nm -D seemd | grep -q ' g_string_free_and_steal$'; then
echo "Error: seemd links against g_string_free_and_steal (too-new GLib ABI for jammy/mint 21.x)" >&2
exit 1
fi
@@ -41,7 +41,7 @@ jobs:
ARCH="$(dpkg --print-architecture)"
MAINTAINER="${GITHUB_REPOSITORY_OWNER} <noreply@github.com>"
PKGDIR="dist/deb/viewmd_${VERSION}_${ARCH}"
PKGDIR="dist/deb/seemd_${VERSION}_${ARCH}"
rm -rf "$PKGDIR"
mkdir -p "$PKGDIR/DEBIAN"
@@ -57,7 +57,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: viewmd-deb
name: seemd-deb
path: dist/deb/*.deb
rpm:
@@ -68,7 +68,7 @@ jobs:
run: |
dnf -y install \
git make gcc rpm-build \
gtk3-devel pkgconf-pkg-config
gtk3-devel webkit2gtk4.1-devel pkgconf-pkg-config
- uses: actions/checkout@v4
with:
@@ -87,7 +87,7 @@ jobs:
TAG="${{ github.event.release.tag_name || github.ref_name }}"
VERSION="${TAG#v}"
mkdir -p dist
git archive --format=tar.gz --prefix="viewmd-${VERSION}/" -o "dist/viewmd-${VERSION}.tar.gz" HEAD
git archive --format=tar.gz --prefix="seemd-${VERSION}/" -o "dist/seemd-${VERSION}.tar.gz" HEAD
- name: Build .rpm
run: |
@@ -98,25 +98,25 @@ jobs:
TOPDIR="$PWD/dist/rpmbuild"
mkdir -p "$TOPDIR"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp "dist/viewmd-${VERSION}.tar.gz" "$TOPDIR/SOURCES/"
cp "dist/seemd-${VERSION}.tar.gz" "$TOPDIR/SOURCES/"
sed \
-e "s/@VERSION@/${VERSION}/g" \
-e "s|@URL@|${URL}|g" \
packaging/rpm/viewmd.spec.in > "$TOPDIR/SPECS/viewmd.spec"
packaging/rpm/seemd.spec.in > "$TOPDIR/SPECS/seemd.spec"
rpmbuild \
--define "_topdir ${TOPDIR}" \
-bb "$TOPDIR/SPECS/viewmd.spec"
-bb "$TOPDIR/SPECS/seemd.spec"
mkdir -p dist/rpm
# Only publish the main runtime RPM (exclude -debuginfo/-debugsource).
find "$TOPDIR/RPMS" -type f -name 'viewmd-[0-9]*.rpm' -exec cp -v -t dist/rpm {} +
find "$TOPDIR/RPMS" -type f -name 'seemd-[0-9]*.rpm' -exec cp -v -t dist/rpm {} +
- uses: actions/upload-artifact@v4
with:
name: viewmd-rpm
path: dist/rpm/viewmd-[0-9]*.rpm
name: seemd-rpm
path: dist/rpm/seemd-[0-9]*.rpm
release-assets:
runs-on: ubuntu-latest
@@ -126,7 +126,7 @@ jobs:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: viewmd-*
pattern: seemd-*
merge-multiple: true
path: dist
@@ -135,4 +135,4 @@ jobs:
with:
files: |
dist/*.deb
dist/viewmd-[0-9]*.rpm
dist/seemd-[0-9]*.rpm
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Robert Fulton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8 -7
View File
@@ -1,6 +1,7 @@
CC = gcc
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags gtk+-3.0`
LDFLAGS = `pkg-config --libs gtk+-3.0`
PKGS = gtk+-3.0 webkit2gtk-4.1
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags $(PKGS)`
LDFLAGS = `pkg-config --libs $(PKGS)`
SRCDIR = src
OBJDIR = obj
@@ -8,7 +9,7 @@ BINDIR = .
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(OBJDIR)/md4c.o
TARGET = $(BINDIR)/viewmd
TARGET = $(BINDIR)/seemd
PREFIX ?= /usr/local
DESTDIR ?=
@@ -37,12 +38,12 @@ clean:
rm -rf $(OBJDIR) $(TARGET)
install: $(TARGET)
install -Dm755 $(TARGET) $(DESTDIR)$(bindir)/viewmd
install -Dm644 assets/viewmd.desktop $(DESTDIR)$(applicationsdir)/viewmd.desktop
install -Dm755 $(TARGET) $(DESTDIR)$(bindir)/seemd
install -Dm644 assets/seemd.desktop $(DESTDIR)$(applicationsdir)/seemd.desktop
uninstall:
rm -f $(DESTDIR)$(bindir)/viewmd
rm -f $(DESTDIR)$(applicationsdir)/viewmd.desktop
rm -f $(DESTDIR)$(bindir)/seemd
rm -f $(DESTDIR)$(applicationsdir)/seemd.desktop
# Header dependencies
$(OBJDIR)/main.o: $(SRCDIR)/app.h $(SRCDIR)/window.h
+33 -54
View File
@@ -1,16 +1,19 @@
# ViewMD
# 👁 SeeMD
A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing. This project is sort of a companion app/fork of my other markdown project [TrayMD](https://github.com/rabfulton/TrayMD).
A lightweight GTK 3 markdown viewer and editor for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing and WebKitGTK for GitHub-style preview rendering. This project is a fork of [ViewMD](https://github.com/rabfulton/ViewMD) by NotAlexNoyle.
![ViewMD](assets/screenshot.png)
![SeeMD](assets/seemd.png)
## Features
- **Native GTK viewer** - No webviews
- **Read-only rendering** - Focused on viewing markdown files
- **Minimal UI** - Clean toolbar with open and settings buttons
- **Lightweight** - Pure C, no web technologies, fast startup
- **GitHub-style preview** - Rendered Markdown and inline HTML are displayed through WebKitGTK with GitHub-like markdown styling
- **Read-only preview** - Rendered Markdown stays non-editable until source mode is enabled
- **Source editing mode** - Toggle between rendered preview and editable Markdown
- **Minimal UI** - Clean toolbar with open, reload, edit, save, and settings buttons
- **Lightweight** - Pure C with GTK and WebKitGTK, fast startup
- **Hyperlink support** - Left click opens links and internal anchors
- **Document search** - `Ctrl+F` with next/previous match navigation
- **Local image support** - Local images keep natural size unless they need to fit the document window
## Supported Markdown
@@ -27,47 +30,36 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
| `> quote` | Block quote |
| `[text](url)` | Link |
| `~~strike~~` | Strikethrough |
| `| table | row |` | Markdown tables |
| `\| table \| row \|` | Markdown tables |
| `<strong>HTML</strong>` | GitHub-style inline and block HTML rendered in preview mode |
| `---` | Horizontal rule |
Code blocks currently support a beta version of keyword highlighting for fenced languages `c`, `java`, and `python`.
## Installation
### Arch Linux (AUR)
https://aur.archlinux.org/packages/viewmd
### Debian/Ubuntu (.deb)
Install a built `.deb` package with:
```bash
sudo apt install ./viewmd_*.deb
```
### Fedora/RHEL (.rpm)
Install a built `.rpm` package with:
```bash
sudo dnf install ./viewmd-*.rpm
```
Fenced code blocks are syntax-highlighted GitHub-style in the preview for the languages `c`, `java`, `javascript`, `kotlin`, `python`, and `bash`, covering keywords, strings, numbers, and comments.
## Usage
Run `viewmd` to start the application.
Run `seemd` to start the application.
- **Open button**: Open a markdown document
- **Reload button**: Reload the currently open document from disk
- **Edit button**: Toggle editable Markdown source mode (`Ctrl+E`)
- **Save button**: Save the current Markdown source (`Ctrl+S`)
- **Settings button**: Adjust theme, fonts, and markdown accent colors
### Find in Document
- Press `Ctrl+F` to open search.
- Type to highlight matches as you search.
- Press `Enter` for next match and `Shift+Enter` for previous match.
- Press `Esc` to close search.
### Set as Default `.md` Viewer
After installing, associate markdown MIME types with `viewmd.desktop`:
After installing, associate markdown MIME types with `seemd.desktop`:
```bash
xdg-mime default viewmd.desktop text/markdown
xdg-mime default viewmd.desktop text/x-markdown
xdg-mime default seemd.desktop text/markdown
xdg-mime default seemd.desktop text/x-markdown
```
Verify the current default:
@@ -91,8 +83,8 @@ sudo make install
```
This installs:
- Binary to `/usr/local/bin/viewmd`
- Desktop file to `/usr/local/share/applications/viewmd.desktop`
- Binary to `/usr/local/bin/seemd`
- Desktop file to `/usr/local/share/applications/seemd.desktop`
### Uninstallation
@@ -103,32 +95,19 @@ sudo make uninstall
### Arch Linux
```bash
sudo pacman -S gtk3
sudo pacman -S gtk3 webkit2gtk-4.1
```
### Ubuntu/Debian
```bash
sudo apt install libgtk-3-dev
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
```
### Fedora
```bash
sudo dnf install gtk3-devel
sudo dnf install gtk3-devel webkit2gtk4.1-devel
```
## Packaging Templates
- Arch: `packaging/arch/PKGBUILD`
- Debian: `packaging/deb/control.in`
- RPM: `packaging/rpm/viewmd.spec.in`
## License
MIT License
## Other Useful Projects
- TrayMD is an app for taking notes in markdown with live editing [TrayMD](https://github.com/rabfulton/TrayMD)
- Preditor is an image viewer with a similar philosophy to viewmd [preditor](https://github.com/rabfulton/preditor)
- Try my AI panel plugin for XFCE [XFCE Ask](https://github.com/rabfulton/xfce-ask)
- For a feature complete AI application try out [ChatGTK](https://github.com/rabfulton/ChatGTK)
- A lightweight speech to text implementation [Auriscribe](https://github.com/rabfulton/Auriscribe)
- A lightweight local movie database and browser [ReelVault](https://github.com/rabfulton/ReelVault)
Released under the [MIT License](LICENSE).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

@@ -1,7 +1,7 @@
[Desktop Entry]
Name=ViewMD
Name=SeeMD
Comment=Lightweight markdown viewer
Exec=viewmd %F
Exec=seemd %F
Icon=accessories-text-editor
Terminal=false
Type=Application
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+3 -3
View File
@@ -1,11 +1,11 @@
pkgname=viewmd
pkgname=seemd
pkgver=0.r0.g0000000
pkgrel=1
pkgdesc="Lightweight GTK3 markdown viewer"
arch=('x86_64')
url="https://github.com/rabfulton/ViewMD"
url="https://github.com/NotAlexNoyle/SeeMD"
license=('MIT')
depends=('gtk3')
depends=('gtk3' 'webkit2gtk-4.1')
makedepends=('git' 'gcc' 'make' 'pkgconf')
source=("$pkgname::git+$url.git")
sha256sums=('SKIP')
+4 -4
View File
@@ -1,9 +1,9 @@
Package: viewmd
Package: seemd
Version: @VERSION@
Section: utils
Priority: optional
Architecture: @ARCH@
Maintainer: @MAINTAINER@
Depends: libgtk-3-0
Description: ViewMD - lightweight GTK3 markdown viewer
ViewMD is a lightweight native GTK3 markdown viewer.
Depends: libgtk-3-0, libwebkit2gtk-4.1-0
Description: SeeMD - lightweight GTK3 markdown viewer
SeeMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
@@ -1,24 +1,26 @@
Name: viewmd
Name: seemd
Version: @VERSION@
Release: 1%{?dist}
Summary: Lightweight GTK3 markdown viewer
License: MIT
URL: @URL@
Source0: viewmd-%{version}.tar.gz
Source0: seemd-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: make
BuildRequires: pkgconfig
BuildRequires: gtk3-devel
BuildRequires: webkit2gtk4.1-devel
Requires: gtk3
Requires: webkit2gtk4.1
%description
ViewMD is a lightweight native GTK3 markdown viewer.
SeeMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
%prep
%autosetup -n viewmd-%{version}
%autosetup -n seemd-%{version}
%build
make %{?_smp_mflags}
@@ -28,9 +30,9 @@ rm -rf %{buildroot}
make install DESTDIR=%{buildroot} PREFIX=%{_prefix}
%files
%{_bindir}/viewmd
%{_datadir}/applications/viewmd.desktop
%{_bindir}/seemd
%{_datadir}/applications/seemd.desktop
%changelog
* Thu Jan 01 1970 ViewMD Packager <noreply@example.com> - @VERSION@-1
* Thu Jan 01 1970 SeeMD Packager <noreply@example.com> - @VERSION@-1
- Initial package
Executable
BIN
View File
Binary file not shown.
+44 -5
View File
@@ -27,7 +27,7 @@ MarkydApp *markyd_app_new(void) {
flags = (GApplicationFlags)(flags | G_APPLICATION_NON_UNIQUE |
G_APPLICATION_HANDLES_OPEN);
self->gtk_app = gtk_application_new("org.viewmd.app", flags);
self->gtk_app = gtk_application_new("org.seemd.app", flags);
self->current_file_path = NULL;
g_signal_connect(self->gtk_app, "activate", G_CALLBACK(on_activate), self);
@@ -69,10 +69,10 @@ static void markyd_app_update_window_title(MarkydApp *self) {
if (self->current_file_path && self->current_file_path[0] != '\0') {
gchar *base = g_path_get_basename(self->current_file_path);
title = g_strdup_printf("ViewMD - %s", base);
title = g_strdup_printf("SeeMD - %s", base);
g_free(base);
} else {
title = g_strdup("ViewMD");
title = g_strdup("SeeMD");
}
gtk_window_set_title(GTK_WINDOW(self->window->window), title);
@@ -111,7 +111,7 @@ static void on_open(GtkApplication *gtk_app, GFile **files, gint n_files,
}
if (!opened && n_files > 0) {
g_printerr("ViewMD: unable to open provided file(s)\n");
g_printerr("SeeMD: unable to open provided file(s)\n");
}
markyd_window_show(self->window);
@@ -126,7 +126,7 @@ static void markyd_app_ensure_window(MarkydApp *self) {
self->editor = self->window->editor;
markyd_editor_set_content(self->editor,
"# ViewMD\n\nUse the Open button to load a markdown document.");
"# SeeMD\n\nUse the Open button to load a markdown document.");
markyd_app_update_window_title(self);
}
@@ -156,6 +156,45 @@ gboolean markyd_app_open_file(MarkydApp *self, const gchar *path) {
return TRUE;
}
gboolean markyd_app_save_file(MarkydApp *self, const gchar *path) {
gchar *content;
GError *error = NULL;
if (!self || !self->editor || !path || path[0] == '\0') {
return FALSE;
}
content = markyd_editor_get_content(self->editor);
if (!g_file_set_contents(path, content ? content : "", -1, &error)) {
if (error) {
g_printerr("Failed to save markdown file '%s': %s\n", path, error->message);
g_error_free(error);
}
g_free(content);
return FALSE;
}
g_free(content);
g_free(self->current_file_path);
self->current_file_path = g_strdup(path);
markyd_app_update_window_title(self);
return TRUE;
}
gboolean markyd_app_save_current_file(MarkydApp *self) {
gchar *path;
gboolean ok;
if (!self || !self->current_file_path || self->current_file_path[0] == '\0') {
return FALSE;
}
path = g_strdup(self->current_file_path);
ok = markyd_app_save_file(self, path);
g_free(path);
return ok;
}
const gchar *markyd_app_get_current_path(MarkydApp *self) {
if (!self) {
return NULL;
+2
View File
@@ -25,6 +25,8 @@ int markyd_app_run(MarkydApp *app, int argc, char **argv);
/* Document management */
gboolean markyd_app_open_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_current_file(MarkydApp *app);
/* Utility */
const gchar *markyd_app_get_current_path(MarkydApp *app);
+218 -6
View File
@@ -76,6 +76,84 @@ static const MarkydKeywordGroup py_groups[] = {
{MARKYD_TAG_CODE_KW_C, py_kw_group_c, G_N_ELEMENTS(py_kw_group_c)},
};
static const gchar *const js_kw_group_a[] = {
"await", "break", "case", "catch", "continue", "default",
"do", "else", "finally", "for", "if", "in",
"instanceof", "of", "return", "switch", "throw", "try",
"while", "with", "yield",
};
static const gchar *const js_kw_group_b[] = {
"async", "class", "const", "debugger", "delete", "export",
"extends","function","import", "let", "new", "static",
"super", "this", "typeof", "var", "void",
};
static const gchar *const js_kw_group_c[] = {
"false", "Infinity", "NaN", "null", "true", "undefined",
};
static const MarkydKeywordGroup js_groups[] = {
{MARKYD_TAG_CODE_KW_A, js_kw_group_a, G_N_ELEMENTS(js_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, js_kw_group_b, G_N_ELEMENTS(js_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, js_kw_group_c, G_N_ELEMENTS(js_kw_group_c)},
};
static const gchar *const kotlin_kw_group_a[] = {
"break", "catch", "continue", "do", "else", "finally",
"for", "if", "in", "is", "return","throw",
"try", "when", "while",
};
static const gchar *const kotlin_kw_group_b[] = {
"abstract", "actual", "annotation", "as", "by",
"class", "companion", "const", "constructor","crossinline",
"data", "enum", "expect", "external", "final",
"fun", "import", "infix", "init", "inline",
"inner", "interface", "internal", "lateinit", "noinline",
"object", "open", "operator", "out", "override",
"package", "private", "protected", "public", "reified",
"sealed", "super", "suspend", "tailrec", "this",
"typealias", "val", "var", "vararg", "where",
};
static const gchar *const kotlin_kw_group_c[] = {
"false", "null", "true", "Any", "Boolean", "Byte", "Char",
"Double","Float","Int", "Long", "Nothing", "Short", "String",
"Unit",
};
static const MarkydKeywordGroup kotlin_groups[] = {
{MARKYD_TAG_CODE_KW_A, kotlin_kw_group_a, G_N_ELEMENTS(kotlin_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, kotlin_kw_group_b, G_N_ELEMENTS(kotlin_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, kotlin_kw_group_c, G_N_ELEMENTS(kotlin_kw_group_c)},
};
static const gchar *const bash_kw_group_a[] = {
"case", "do", "done", "elif", "else", "esac", "fi",
"for", "function","if", "in", "select","then", "time",
"until","while",
};
static const gchar *const bash_kw_group_b[] = {
"alias", "bg", "break", "cd", "command", "continue",
"declare", "echo", "eval", "exec", "exit", "export",
"getopts", "hash", "jobs", "kill", "let", "local",
"printf", "pwd", "read", "readonly","return", "set",
"shift", "source", "test", "trap", "type", "typeset",
"ulimit", "umask", "unalias", "unset", "wait",
};
static const gchar *const bash_kw_group_c[] = {
"false", "true",
};
static const MarkydKeywordGroup bash_groups[] = {
{MARKYD_TAG_CODE_KW_A, bash_kw_group_a, G_N_ELEMENTS(bash_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, bash_kw_group_b, G_N_ELEMENTS(bash_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, bash_kw_group_c, G_N_ELEMENTS(bash_kw_group_c)},
};
static gboolean is_ascii_identifier_char(gchar c) {
return (c == '_') || g_ascii_isalnum((guchar)c);
}
@@ -554,23 +632,43 @@ static void scan_line_c_like(const MarkydLanguageHighlight *language,
}
if (in_block_comment) {
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
continue;
gint start_char_index = char_index;
while (*p) {
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
break;
}
advance_utf8_char(&p, &char_index);
}
advance_utf8_char(&p, &char_index);
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
continue;
}
if (p[0] == '/' && p[1] == '/') {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
if (p[0] == '/' && p[1] == '*') {
gint start_char_index = char_index;
p += 2;
char_index += 2;
in_block_comment = TRUE;
while (*p) {
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
break;
}
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
continue;
}
if (allow_java_text_blocks && starts_with_triple_quote(p)) {
@@ -704,6 +802,11 @@ static void scan_line_python(const MarkydLanguageHighlight *language,
}
if (p[0] == '#') {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
@@ -827,11 +930,120 @@ static void scan_line_python(const MarkydLanguageHighlight *language,
}
}
/* Bash: '#' comments (position-sensitive), single/double-quoted strings, and
* keyword/builtin identifiers. No multi-line state is tracked. */
static void scan_line_bash(const MarkydLanguageHighlight *language,
const gchar *line, MarkydCodeScanState *state,
MarkydCodeTokenCallback on_token,
gpointer user_data) {
const gchar *p;
gint char_index = 0;
gchar prev = '\0';
if (!language || !line || !state || !on_token) {
return;
}
p = line;
while (*p) {
/* A '#' starts a comment only at the start of a word (line start or after
* whitespace), so tokens like "$#" and "a#b" are left alone. */
if (p[0] == '#' && (p == line || prev == ' ' || prev == '\t')) {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
if (p[0] == '\'') {
gint start_char_index = char_index;
p++;
char_index++;
while (*p && p[0] != '\'') {
advance_utf8_char(&p, &char_index);
}
if (*p == '\'') {
p++;
char_index++;
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
prev = '\'';
continue;
}
if (p[0] == '"') {
gint start_char_index = char_index;
p++;
char_index++;
while (*p) {
if (p[0] == '\\' && p[1]) {
p++;
char_index++;
advance_utf8_char(&p, &char_index);
continue;
}
if (p[0] == '"') {
p++;
char_index++;
break;
}
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
prev = '"';
continue;
}
{
gunichar c = g_utf8_get_char(p);
const gchar *next = g_utf8_next_char(p);
if (is_identifier_start(c)) {
const gchar *token_start = p;
gint start_char_index = char_index;
p = next;
char_index++;
while (*p) {
gunichar tc = g_utf8_get_char(p);
const gchar *tnext = g_utf8_next_char(p);
if (!is_identifier_char(tc)) {
break;
}
p = tnext;
char_index++;
}
gsize token_len = (gsize)(p - token_start);
const gchar *tag_name =
lookup_keyword_tag(language, token_start, token_len);
if (tag_name) {
on_token(start_char_index, char_index, tag_name, user_data);
}
prev = 'a';
continue;
}
}
prev = *p;
advance_utf8_char(&p, &char_index);
}
}
static const MarkydLanguageHighlight languages[] = {
{"c", c_groups, G_N_ELEMENTS(c_groups), scan_line_c},
{"java", java_groups, G_N_ELEMENTS(java_groups), scan_line_java},
{"python", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
{"py", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
{"javascript", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
{"js", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
{"kotlin", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
{"kt", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
{"bash", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
{"sh", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
{"shell", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
};
const MarkydLanguageHighlight *
+1
View File
@@ -7,6 +7,7 @@
#define MARKYD_TAG_CODE_KW_B "code_kw_b"
#define MARKYD_TAG_CODE_KW_C "code_kw_c"
#define MARKYD_TAG_CODE_LITERAL "code_literal"
#define MARKYD_TAG_CODE_COMMENT "code_comment"
typedef struct _MarkydKeywordGroup {
const gchar *tag_name;
+1 -1
View File
@@ -50,7 +50,7 @@ void config_free(MarkydConfig *cfg) {
const gchar *config_get_path(void) {
if (!config_path) {
const gchar *config_dir = g_get_user_config_dir();
gchar *new_app_dir = g_build_filename(config_dir, "viewmd", NULL);
gchar *new_app_dir = g_build_filename(config_dir, "seemd", NULL);
g_mkdir_with_parents(new_app_dir, 0755);
config_path = g_build_filename(new_app_dir, "config.ini", NULL);
+689 -26
View File
@@ -1,6 +1,8 @@
#include "editor.h"
#include "app.h"
#include "markdown.h"
#include <string.h>
#include <webkit2/webkit2.h>
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data);
@@ -10,9 +12,22 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
gpointer user_data);
static void apply_markdown(MarkydEditor *self);
static void schedule_markdown_apply(MarkydEditor *self);
static void render_image_widgets(MarkydEditor *self);
static void render_table_widgets(MarkydEditor *self);
static const gchar *TABLE_WIDGET_DATA_KEY = "viewmd-table-widget";
static void refresh_image_widget_scales(MarkydEditor *self);
static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path);
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data);
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data);
static void sync_source_from_buffer(MarkydEditor *self);
static void show_source_buffer(MarkydEditor *self);
static void render_preview(MarkydEditor *self);
static gchar *get_preview_base_uri(MarkydEditor *self);
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data);
static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
GSList *tags;
@@ -26,7 +41,7 @@ static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
for (GSList *node = tags; node != NULL; node = node->next) {
GtkTextTag *tag = GTK_TEXT_TAG(node->data);
const gchar *stored =
(const gchar *)g_object_get_data(G_OBJECT(tag), VIEWMD_LINK_URL_DATA);
(const gchar *)g_object_get_data(G_OBJECT(tag), SEEMD_LINK_URL_DATA);
if (stored && stored[0] != '\0') {
url = g_strdup(stored);
break;
@@ -105,24 +120,43 @@ static gboolean scroll_to_markdown_anchor(MarkydEditor *self,
return TRUE;
}
static void set_link_cursor(MarkydEditor *self, gboolean active) {
GdkWindow *win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
static void set_editor_mouse_cursor(MarkydEditor *self, gboolean over_link) {
GdkWindow *win;
GdkDisplay *display;
GdkCursor *cursor;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (!win) {
return;
}
if (active) {
GdkDisplay *display = gdk_window_get_display(win);
GdkCursor *cursor = gdk_cursor_new_from_name(display, "pointer");
if (!cursor) {
cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
}
gdk_window_set_cursor(win, cursor);
if (cursor) {
g_object_unref(cursor);
}
} else {
display = gdk_window_get_display(win);
cursor = gdk_cursor_new_from_name(display, over_link ? "pointer" : "text");
if (!cursor) {
cursor = gdk_cursor_new_for_display(display, over_link ? GDK_HAND2 : GDK_XTERM);
}
gdk_window_set_cursor(win, cursor);
if (cursor) {
g_object_unref(cursor);
}
}
static void clear_editor_mouse_cursor(MarkydEditor *self) {
GdkWindow *win;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (win) {
gdk_window_set_cursor(win, NULL);
}
}
@@ -131,14 +165,271 @@ static void apply_markdown(MarkydEditor *self) {
if (!self) {
return;
}
if (self->edit_mode) {
return;
}
render_preview(self);
self->updating_tags = TRUE;
markdown_apply_tags(self->buffer,
self->source_content ? self->source_content : "");
render_image_widgets(self);
render_table_widgets(self);
refresh_image_widget_scales(self);
self->updating_tags = FALSE;
}
static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path) {
gchar *path = NULL;
if (out_path) {
*out_path = NULL;
}
if (!self || !src || src[0] == '\0' || !out_path) {
return FALSE;
}
if (g_uri_parse_scheme(src) != NULL) {
if (g_str_has_prefix(src, "file://")) {
path = g_filename_from_uri(src, NULL, NULL);
} else {
return FALSE;
}
} else if (g_path_is_absolute(src)) {
path = g_strdup(src);
} else {
const gchar *current_path = markyd_app_get_current_path(self->app);
if (current_path && current_path[0] != '\0') {
gchar *dir = g_path_get_dirname(current_path);
path = g_build_filename(dir, src, NULL);
g_free(dir);
} else {
path = g_strdup(src);
}
}
if (!path) {
return FALSE;
}
if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
g_free(path);
return FALSE;
}
*out_path = path;
return TRUE;
}
static gint get_image_max_width(MarkydEditor *self) {
GtkAllocation alloc;
GdkRectangle visible = {0, 0, 0, 0};
GtkTextView *view;
gint width;
if (!self || !self->text_view) {
return 0;
}
view = GTK_TEXT_VIEW(self->text_view);
gtk_text_view_get_visible_rect(view, &visible);
width = visible.width;
if (width <= 0) {
gtk_widget_get_allocation(self->text_view, &alloc);
width = alloc.width;
}
width -= gtk_text_view_get_left_margin(view);
width -= gtk_text_view_get_right_margin(view);
return MAX(width, 0);
}
static void compute_image_display_size(gint orig_width, gint orig_height,
gint requested_width,
gint requested_height, gint max_width,
gint *out_width, gint *out_height) {
gint width = orig_width;
gint height = orig_height;
if (orig_width <= 0 || orig_height <= 0) {
width = MAX(requested_width, 1);
height = MAX(requested_height, 1);
} else if (requested_width > 0 && requested_height > 0) {
width = requested_width;
height = requested_height;
} else if (requested_width > 0) {
width = requested_width;
height = (gint)(((gdouble)orig_height * (gdouble)width) /
(gdouble)orig_width + 0.5);
} else if (requested_height > 0) {
height = requested_height;
width = (gint)(((gdouble)orig_width * (gdouble)height) /
(gdouble)orig_height + 0.5);
}
width = MAX(width, 1);
height = MAX(height, 1);
if (max_width > 0 && width > max_width) {
height = (gint)(((gdouble)height * (gdouble)max_width) /
(gdouble)width + 0.5);
width = max_width;
}
if (out_width) {
*out_width = MAX(width, 1);
}
if (out_height) {
*out_height = MAX(height, 1);
}
}
static void scale_image_widget(GtkWidget *widget, gint max_width) {
GtkWidget *image;
GdkPixbuf *orig;
gint ow;
gint oh;
gint requested_width;
gint requested_height;
gint display_width;
gint display_height;
if (!widget) {
return;
}
image = g_object_get_data(G_OBJECT(widget), "seemd-image-widget-child");
orig = g_object_get_data(G_OBJECT(widget), "seemd-image-orig-pixbuf");
if (!GTK_IS_IMAGE(image) || !GDK_IS_PIXBUF(orig)) {
return;
}
ow = gdk_pixbuf_get_width(orig);
oh = gdk_pixbuf_get_height(orig);
if (ow <= 0 || oh <= 0) {
return;
}
requested_width = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(widget), "seemd-image-requested-width"));
requested_height = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(widget), "seemd-image-requested-height"));
compute_image_display_size(ow, oh, requested_width, requested_height, max_width,
&display_width, &display_height);
if (display_width == ow && display_height == oh) {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
} else {
GdkPixbuf *scaled = gdk_pixbuf_scale_simple(
orig, display_width, display_height, GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled ? scaled : orig);
if (scaled) {
g_object_unref(scaled);
}
}
}
static void refresh_image_widget_scales(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
gint max_width;
if (!self || !self->buffer) {
return;
}
max_width = get_image_max_width(self);
gtk_text_buffer_get_bounds(self->buffer, &iter, &end);
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA);
if (image_widget) {
scale_image_widget(image_widget, max_width);
}
}
gtk_text_iter_forward_char(&iter);
}
}
static void render_image_widgets(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
gint max_width;
if (!self || !self->buffer || !self->text_view) {
return;
}
max_width = get_image_max_width(self);
gtk_text_buffer_get_bounds(self->buffer, &iter, &end);
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA);
if (!image_widget) {
const gchar *src =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_SRC_DATA);
const gchar *alt =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ALT_DATA);
gint requested_width = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDTH_DATA));
gint requested_height = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_HEIGHT_DATA));
gchar *path = NULL;
if (resolve_image_source_path(self, src, &path)) {
GError *err = NULL;
GdkPixbuf *orig = gdk_pixbuf_new_from_file(path, &err);
if (orig) {
GtkWidget *event_box = gtk_event_box_new();
GtkWidget *image = gtk_image_new();
gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
gtk_widget_set_halign(event_box, GTK_ALIGN_START);
gtk_container_add(GTK_CONTAINER(event_box), image);
g_object_set_data(G_OBJECT(event_box), "seemd-image-widget-child",
image);
g_object_set_data_full(G_OBJECT(event_box), "seemd-image-orig-pixbuf",
orig, g_object_unref);
g_object_set_data(G_OBJECT(event_box), "seemd-image-requested-width",
GINT_TO_POINTER(requested_width));
g_object_set_data(G_OBJECT(event_box), "seemd-image-requested-height",
GINT_TO_POINTER(requested_height));
scale_image_widget(event_box, max_width);
image_widget = event_box;
} else if (err) {
g_error_free(err);
}
}
if (!image_widget) {
GtkWidget *fallback = gtk_label_new(alt && alt[0] != '\0' ? alt : src);
gtk_widget_set_halign(fallback, GTK_ALIGN_START);
gtk_style_context_add_class(gtk_widget_get_style_context(fallback),
"dim-label");
image_widget = fallback;
}
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view),
image_widget, anchor);
gtk_widget_show_all(image_widget);
g_object_set_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA, image_widget);
g_free(path);
} else {
scale_image_widget(image_widget, max_width);
}
}
gtk_text_iter_forward_char(&iter);
}
}
static void render_table_widgets(MarkydEditor *self) {
GtkTextIter iter;
GtkTextIter end;
@@ -151,15 +442,16 @@ static void render_table_widgets(MarkydEditor *self) {
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table = g_object_get_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY);
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table =
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA);
if (!table) {
table = markdown_create_table_widget(anchor);
if (table) {
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view), table,
anchor);
gtk_widget_show_all(table);
g_object_set_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY, table);
g_object_set_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA, table);
}
}
}
@@ -178,20 +470,274 @@ static void schedule_markdown_apply(MarkydEditor *self) {
if (!self || self->updating_tags || self->markdown_idle_id != 0) {
return;
}
if (self->edit_mode) {
return;
}
self->markdown_idle_id =
g_idle_add_full(G_PRIORITY_LOW, apply_markdown_idle, self, NULL);
}
void markyd_editor_refresh(MarkydEditor *self) { schedule_markdown_apply(self); }
static void sync_source_from_buffer(MarkydEditor *self) {
GtkTextIter start;
GtkTextIter end;
gchar *text;
if (!self || !self->buffer) {
return;
}
gtk_text_buffer_get_bounds(self->buffer, &start, &end);
text = gtk_text_buffer_get_text(self->buffer, &start, &end, FALSE);
g_free(self->source_content);
self->source_content = text ? text : g_strdup("");
}
static void show_source_buffer(MarkydEditor *self) {
if (!self || !self->buffer) {
return;
}
self->updating_tags = TRUE;
gtk_text_buffer_set_text(self->buffer,
self->source_content ? self->source_content : "", -1);
self->updating_tags = FALSE;
}
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
(void)buffer;
if (!self || self->updating_tags || !self->edit_mode) {
return;
}
sync_source_from_buffer(self);
}
static gchar *get_preview_base_uri(MarkydEditor *self) {
const gchar *current_path;
gchar *dir;
GFile *dir_file;
gchar *uri;
gchar *base_uri;
current_path = self && self->app ? markyd_app_get_current_path(self->app) : NULL;
if (current_path && current_path[0] != '\0') {
dir = g_path_get_dirname(current_path);
} else {
dir = g_get_current_dir();
}
dir_file = g_file_new_for_path(dir);
uri = g_file_get_uri(dir_file);
base_uri = g_str_has_suffix(uri, "/") ? g_strdup(uri) : g_strconcat(uri, "/", NULL);
g_free(uri);
g_object_unref(dir_file);
g_free(dir);
return base_uri;
}
static void render_preview(MarkydEditor *self) {
gchar *base_uri;
gchar *html;
if (!self || !self->preview_view) {
return;
}
base_uri = get_preview_base_uri(self);
html = markdown_render_github_html(self->source_content ? self->source_content : "",
base_uri);
g_free(self->preview_base_uri);
self->preview_base_uri = g_strdup(base_uri);
webkit_web_view_load_html(WEBKIT_WEB_VIEW(self->preview_view), html, base_uri);
g_free(html);
g_free(base_uri);
}
static gboolean preview_uri_is_same_document(MarkydEditor *self,
const gchar *uri) {
const gchar *fragment;
if (!uri || uri[0] == '\0') {
return TRUE;
}
if (uri[0] == '#') {
return TRUE;
}
if (g_str_has_prefix(uri, "about:")) {
return TRUE;
}
fragment = strchr(uri, '#');
if (fragment && self && self->preview_base_uri &&
g_str_has_prefix(uri, self->preview_base_uri)) {
return TRUE;
}
return FALSE;
}
static gboolean path_is_markdown_file(const gchar *path) {
gchar *lower;
gboolean result;
if (!path) {
return FALSE;
}
lower = g_ascii_strdown(path, -1);
result = g_str_has_suffix(lower, ".md") ||
g_str_has_suffix(lower, ".markdown");
g_free(lower);
return result;
}
static gboolean open_local_markdown_link(MarkydEditor *self, const gchar *uri) {
gchar *path;
gchar *fragment;
gboolean opened = FALSE;
if (!self || !self->app || !uri || !g_str_has_prefix(uri, "file://")) {
return FALSE;
}
path = g_filename_from_uri(uri, NULL, NULL);
if (!path) {
return FALSE;
}
fragment = strchr(path, '#');
if (fragment) {
*fragment = '\0';
}
if (path_is_markdown_file(path) && g_file_test(path, G_FILE_TEST_EXISTS)) {
opened = markyd_app_open_file(self->app, path);
}
g_free(path);
return opened;
}
static void open_preview_uri_externally(GtkWidget *widget, const gchar *uri) {
GtkWidget *toplevel;
GError *error = NULL;
if (!uri || uri[0] == '\0') {
return;
}
toplevel = gtk_widget_get_toplevel(widget);
if (!gtk_show_uri_on_window(GTK_IS_WINDOW(toplevel) ? GTK_WINDOW(toplevel)
: NULL,
uri, GDK_CURRENT_TIME, &error)) {
if (error) {
g_printerr("Failed to open link '%s': %s\n", uri, error->message);
g_clear_error(&error);
}
}
}
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
WebKitNavigationPolicyDecision *navigation_decision;
WebKitNavigationAction *action;
WebKitURIRequest *request;
const gchar *uri;
if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
return FALSE;
}
navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
action = webkit_navigation_policy_decision_get_navigation_action(
navigation_decision);
if (!action ||
webkit_navigation_action_get_navigation_type(action) !=
WEBKIT_NAVIGATION_TYPE_LINK_CLICKED) {
return FALSE;
}
request = webkit_navigation_action_get_request(action);
uri = request ? webkit_uri_request_get_uri(request) : NULL;
if (preview_uri_is_same_document(self, uri)) {
return FALSE;
}
if (open_local_markdown_link(self, uri)) {
webkit_policy_decision_ignore(decision);
return TRUE;
}
open_preview_uri_externally(GTK_WIDGET(web_view), uri);
webkit_policy_decision_ignore(decision);
return TRUE;
}
void markyd_editor_refresh(MarkydEditor *self) {
if (!self) {
return;
}
if (self->edit_mode) {
return;
}
schedule_markdown_apply(self);
}
void markyd_editor_set_edit_mode(MarkydEditor *self, gboolean edit_mode) {
if (!self || self->edit_mode == edit_mode) {
return;
}
if (edit_mode) {
if (self->markdown_idle_id != 0) {
g_source_remove(self->markdown_idle_id);
self->markdown_idle_id = 0;
}
self->edit_mode = TRUE;
show_source_buffer(self);
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "source");
gtk_widget_grab_focus(self->text_view);
} else {
sync_source_from_buffer(self);
self->edit_mode = FALSE;
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
schedule_markdown_apply(self);
}
}
gboolean markyd_editor_get_edit_mode(MarkydEditor *self) {
return self ? self->edit_mode : FALSE;
}
MarkydEditor *markyd_editor_new(MarkydApp *app) {
MarkydEditor *self = g_new0(MarkydEditor, 1);
WebKitSettings *settings;
self->app = app;
self->source_content = g_strdup("");
self->updating_tags = FALSE;
self->markdown_idle_id = 0;
self->edit_mode = FALSE;
self->preview_base_uri = NULL;
self->widget = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(self->widget),
GTK_STACK_TRANSITION_TYPE_NONE);
gtk_widget_set_hexpand(self->widget, TRUE);
gtk_widget_set_vexpand(self->widget, TRUE);
self->text_view = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self->text_view),
@@ -203,8 +749,28 @@ MarkydEditor *markyd_editor_new(MarkydApp *app) {
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
self->source_scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->source_scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(self->source_scroll), self->text_view);
self->preview_view = webkit_web_view_new();
gtk_widget_set_hexpand(self->preview_view, TRUE);
gtk_widget_set_vexpand(self->preview_view, TRUE);
settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(self->preview_view));
webkit_settings_set_enable_javascript(settings, TRUE);
webkit_settings_set_enable_javascript_markup(settings, FALSE);
webkit_settings_set_auto_load_images(settings, TRUE);
g_signal_connect(self->preview_view, "decide-policy",
G_CALLBACK(on_preview_decide_policy), self);
gtk_stack_add_named(GTK_STACK(self->widget), self->preview_view, "preview");
gtk_stack_add_named(GTK_STACK(self->widget), self->source_scroll, "source");
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
self->buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->text_view));
markdown_init_tags(self->buffer);
g_signal_connect(self->buffer, "changed", G_CALLBACK(on_buffer_changed), self);
gtk_widget_add_events(self->text_view, GDK_POINTER_MOTION_MASK |
GDK_LEAVE_NOTIFY_MASK |
@@ -215,10 +781,20 @@ MarkydEditor *markyd_editor_new(MarkydApp *app) {
G_CALLBACK(on_motion_notify), self);
g_signal_connect(self->text_view, "leave-notify-event",
G_CALLBACK(on_leave_notify), self);
g_signal_connect(self->text_view, "size-allocate",
G_CALLBACK(on_text_view_size_allocate), self);
return self;
}
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
(void)widget;
(void)allocation;
refresh_image_widget_scales(self);
}
void markyd_editor_free(MarkydEditor *self) {
if (!self) {
return;
@@ -228,6 +804,7 @@ void markyd_editor_free(MarkydEditor *self) {
self->markdown_idle_id = 0;
}
g_free(self->source_content);
g_free(self->preview_base_uri);
g_free(self);
}
@@ -238,24 +815,42 @@ void markyd_editor_set_content(MarkydEditor *self, const gchar *content) {
g_free(self->source_content);
self->source_content = g_strdup(content ? content : "");
schedule_markdown_apply(self);
if (self->edit_mode) {
show_source_buffer(self);
} else {
schedule_markdown_apply(self);
}
}
gchar *markyd_editor_get_content(MarkydEditor *self) {
if (!self) {
return g_strdup("");
}
if (self->edit_mode) {
sync_source_from_buffer(self);
}
return g_strdup(self->source_content ? self->source_content : "");
}
GtkWidget *markyd_editor_get_widget(MarkydEditor *self) {
return self->text_view;
return self ? self->widget : NULL;
}
void markyd_editor_focus(MarkydEditor *self) {
GtkTextMark *insert_mark;
if (!self || !self->text_view || !self->buffer) {
if (!self) {
return;
}
if (!self->edit_mode) {
if (self->preview_view) {
gtk_widget_grab_focus(self->preview_view);
}
return;
}
if (!self->text_view || !self->buffer) {
return;
}
@@ -267,6 +862,74 @@ void markyd_editor_focus(MarkydEditor *self) {
}
}
void markyd_editor_scroll_top(MarkydEditor *self) {
GtkTextIter start;
if (!self) {
return;
}
if (!self->edit_mode && self->preview_view) {
webkit_web_view_evaluate_javascript(WEBKIT_WEB_VIEW(self->preview_view),
"window.scrollTo(0, 0);", -1, NULL,
NULL, NULL, NULL, NULL);
return;
}
if (!self->buffer || !self->text_view) {
return;
}
gtk_text_buffer_get_start_iter(self->buffer, &start);
gtk_text_buffer_place_cursor(self->buffer, &start);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->text_view), &start, 0.0,
FALSE, 0.0, 0.0);
}
void markyd_editor_find(MarkydEditor *self, const gchar *query) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view || !query ||
query[0] == '\0') {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search(controller, query,
WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
WEBKIT_FIND_OPTIONS_WRAP_AROUND,
G_MAXUINT);
}
void markyd_editor_find_next(MarkydEditor *self, gboolean previous) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
if (previous) {
webkit_find_controller_search_previous(controller);
} else {
webkit_find_controller_search_next(controller);
}
}
void markyd_editor_find_clear(MarkydEditor *self) {
WebKitFindController *controller;
if (!self || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search_finish(controller);
}
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
@@ -334,7 +997,7 @@ static gboolean on_motion_notify(GtkWidget *widget, GdkEventMotion *event,
(gint)event->y, &bx, &by);
gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, bx, by);
set_link_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url));
set_editor_mouse_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url));
g_free(url);
return FALSE;
}
@@ -344,6 +1007,6 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
MarkydEditor *self = (MarkydEditor *)user_data;
(void)widget;
(void)event;
set_link_cursor(self, FALSE);
clear_editor_mouse_cursor(self);
return FALSE;
}
+18
View File
@@ -6,7 +6,10 @@
typedef struct _MarkydApp MarkydApp;
typedef struct _MarkydEditor {
GtkWidget *widget;
GtkWidget *source_scroll;
GtkWidget *text_view;
GtkWidget *preview_view;
GtkTextBuffer *buffer;
MarkydApp *app;
@@ -18,6 +21,11 @@ typedef struct _MarkydEditor {
/* Coalesce markdown re-rendering to idle to avoid invalidating GTK iterators. */
guint markdown_idle_id;
/* TRUE when the buffer is showing editable markdown source instead of preview. */
gboolean edit_mode;
gchar *preview_base_uri;
} MarkydEditor;
/* Lifecycle */
@@ -28,9 +36,19 @@ void markyd_editor_free(MarkydEditor *editor);
void markyd_editor_set_content(MarkydEditor *editor, const gchar *content);
gchar *markyd_editor_get_content(MarkydEditor *editor);
/* Source editing mode */
void markyd_editor_set_edit_mode(MarkydEditor *editor, gboolean edit_mode);
gboolean markyd_editor_get_edit_mode(MarkydEditor *editor);
/* Widget access */
GtkWidget *markyd_editor_get_widget(MarkydEditor *editor);
void markyd_editor_focus(MarkydEditor *editor);
void markyd_editor_scroll_top(MarkydEditor *editor);
/* Preview search */
void markyd_editor_find(MarkydEditor *editor, const gchar *query);
void markyd_editor_find_next(MarkydEditor *editor, gboolean previous);
void markyd_editor_find_clear(MarkydEditor *editor);
/* Force a refresh of markdown styling/rendering (e.g., after settings change). */
void markyd_editor_refresh(MarkydEditor *editor);
+2502 -50
View File
File diff suppressed because it is too large. Load diff
+38 -4
View File
@@ -4,7 +4,7 @@
#include <gtk/gtk.h>
/* GObject data key used to mark hrule child anchors inserted into the buffer. */
#define TRAYMD_HRULE_ANCHOR_DATA "viewmd-hr-anchor"
#define TRAYMD_HRULE_ANCHOR_DATA "seemd-hr-anchor"
/* Initialize markdown tags on a text buffer */
void markdown_init_tags(GtkTextBuffer *buffer);
@@ -13,13 +13,44 @@ void markdown_init_tags(GtkTextBuffer *buffer);
void markdown_update_accent_tags(GtkTextBuffer *buffer);
/* Data key set on per-link metadata tags to store resolved URL/href. */
#define VIEWMD_LINK_URL_DATA "viewmd-link-url"
#define SEEMD_LINK_URL_DATA "seemd-link-url"
/* Prefix for named text marks used as internal heading anchors. */
#define VIEWMD_ANCHOR_MARK_PREFIX "viewmd-anchor-"
#define SEEMD_ANCHOR_MARK_PREFIX "seemd-anchor-"
/* GObject data key used to mark table child anchors with parsed table data. */
#define VIEWMD_TABLE_ANCHOR_DATA "viewmd-table-anchor"
#define SEEMD_TABLE_ANCHOR_DATA "seemd-table-anchor"
/* GObject data key set on table anchors for hidden searchable index metadata. */
#define SEEMD_TABLE_SEARCH_INDEX_DATA "seemd-table-search-index"
/* GObject data key set on table anchors for attached table widget instance. */
#define SEEMD_TABLE_WIDGET_DATA "seemd-table-widget"
/* GObject data keys set on each table cell widget. */
#define SEEMD_TABLE_CELL_ROW_DATA "seemd-table-cell-row"
#define SEEMD_TABLE_CELL_COL_DATA "seemd-table-cell-col"
/* CSS classes for table search highlight states. */
#define SEEMD_TABLE_CELL_MATCH_CLASS "seemd-table-cell-match"
#define SEEMD_TABLE_CELL_CURRENT_CLASS "seemd-table-cell-current"
/* GObject data keys for image anchors and metadata. */
#define SEEMD_IMAGE_ANCHOR_DATA "seemd-image-anchor"
#define SEEMD_IMAGE_SRC_DATA "seemd-image-src"
#define SEEMD_IMAGE_ALT_DATA "seemd-image-alt"
#define SEEMD_IMAGE_WIDGET_DATA "seemd-image-widget"
#define SEEMD_IMAGE_WIDTH_DATA "seemd-image-width"
#define SEEMD_IMAGE_HEIGHT_DATA "seemd-image-height"
typedef struct {
gint row;
gint col;
gint start_offset;
gint end_offset;
} SeemdTableSearchCellRange;
typedef struct {
gint start_offset;
gint end_offset;
GArray *cells; /* SeemdTableSearchCellRange */
} SeemdTableSearchIndex;
/* Normalize heading/link text into anchor slug form. Caller owns result. */
gchar *markdown_normalize_anchor_slug(const gchar *text);
@@ -30,6 +61,9 @@ gchar *markdown_anchor_mark_name(const gchar *fragment);
/* Render markdown source into the buffer and apply markdown styling. */
void markdown_apply_tags(GtkTextBuffer *buffer, const gchar *source);
/* Render markdown source to a complete GitHub-style HTML document. */
gchar *markdown_render_github_html(const gchar *source, const gchar *base_uri);
/* Build a GTK widget for a table anchor, or NULL if not a table anchor. */
GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor);
+1082 -46
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -11,11 +11,20 @@ typedef struct _MarkydWindow {
GtkWidget *header_bar;
GtkWidget *btn_open;
GtkWidget *btn_refresh;
GtkWidget *btn_edit;
GtkWidget *btn_save;
GtkWidget *btn_settings;
GtkWidget *search_revealer;
GtkWidget *search_entry;
GtkWidget *btn_search_prev;
GtkWidget *btn_search_next;
GtkWidget *lbl_search_status;
GtkWidget *lbl_title;
GtkWidget *scroll;
MarkydEditor *editor;
MarkydApp *app;
GArray *search_matches;
gint search_current_index;
} MarkydWindow;
/* Lifecycle */