amu/cmd/amulive/main.go

309 lines
8.1 KiB
Go

/*
AMU: Custom simple markup language
Copyright (C) 2021 Arsen Musayelyan
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 <https://www.gnu.org/licenses/>.
*/
package main
import (
//"fmt"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"runtime"
"strings"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
sourceview "github.com/linuxerwang/sourceview3"
"github.com/pkg/browser"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/sourcegraph/go-webkit2/webkit2"
"go.arsenm.dev/amu/ast"
"go.arsenm.dev/amu/formatter/html"
"go.arsenm.dev/amu/parser"
)
func init() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
var head = `<style>
/* Text formatting
body:not(.cd) {
white-space: pre-line;
}*/
body {
color: %s;
background-color: %s;
word-wrap: break-word;
}
@media print {
body {
color: black;
background-color: white;
}
}
/* Headings */
h1 { margin: 0; }
h2 { margin: 0; }
h3 { margin: 0; }
h4 { margin: 0; }
h5 { margin: 0; }
h6 { margin: 0; }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.css" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/contrib/auto-render.min.js" integrity="sha384-vZTG03m+2yp6N6BNi5iM4rW4oIwk5DfcNdFfxkk9ZWpDriOkXX8voJBFrAO7MpVl" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
<!--script>
function renderMath() {
var math = document.getElementsByClassName("math");
for (var i = 0; i < math.length; i++) {
math[i].innerHTML = katex.renderToString(math[i].innerText, {throwOnError: false});
}
}
</script-->`
const document = `<!DOCTYPE html>
<html>
<head>%s</head>
<body>%s</body>
</html>`
var openedFile string
func main() {
// Lock goroutine to current thread for Gtk
runtime.LockOSThread()
// Initialize Gtk
gtk.Init(nil)
// Create new Gtk window
window, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
log.Fatal().Err(err).Msg("Unable to create window")
}
// Set window title
window.SetTitle("AMULive")
// Stop Gtk main loop when window destroyed
window.Connect("destroy", gtk.MainQuit)
// Create new horizontal box layout with 6px padding
layout, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6)
if err != nil {
log.Fatal().Err(err).Msg("Unable to create box")
}
// Add layout to window
window.Add(layout)
// Create new scrolled winfow
textScrollWindow, err := gtk.ScrolledWindowNew(nil, nil)
if err != nil {
log.Fatal().Err(err).Msg("Unable to create scrolled window")
}
// Create new source view
srcView, err := sourceview.SourceViewNew()
if err != nil {
log.Fatal().Err(err).Msg("Unable to create text virw")
}
// Set tab width to 4
srcView.SetProperty("tab-width", uint(4))
// Set auto indent to true
srcView.SetProperty("auto-indent", true)
// Set show line numbers to true
srcView.SetShowLineNumbers(true)
// Set left margin to 8 (separates text from line numbers)
srcView.SetLeftMargin(8)
// Set monospace to true
srcView.SetMonospace(true)
// Set wrap mode to wrap word char (3)
srcView.SetWrapMode(gtk.WRAP_WORD_CHAR)
// Add text to scrolled window
textScrollWindow.Add(srcView)
// Add scrolled window to layout with no padding
layout.PackStart(textScrollWindow, true, true, 0)
// Get source view style context
styleCtx, err := srcView.GetStyleContext()
if err != nil {
log.Fatal().Err(err).Msg("Unable to get style context of text view")
}
// Get style background color
bgCol, err := styleCtx.GetProperty("background-color", gtk.STATE_FLAG_NORMAL)
if err != nil {
log.Fatal().Err(err).Msg("Unable to get background-color property of style context")
}
// Get style foreground color
fgCol := styleCtx.GetColor(gtk.STATE_FLAG_NORMAL)
// Expand head tag template
head = fmt.Sprintf(head, fgCol, bgCol)
// Create new webview
webkit := webkit2.NewWebView()
// Enable devtools in webview
webkit.Settings().SetProperty("enable-developer-extras", true)
// Load empty base HTML document
webkit.LoadHTML(fmt.Sprintf(document, head, ""), "amu")
// Add webview to layout with no padding
layout.PackStart(webkit, true, true, 0)
// Get source view buffer
srcBuf, err := srcView.GetBuffer()
if err != nil {
log.Fatal().Err(err).Msg("Error getting buffer from text view")
}
// On source view change
srcBuf.Connect("changed", func() {
loadAMU(srcBuf, webkit)
})
// On load change
webkit.Connect("load-changed", func(_ *glib.Object, le webkit2.LoadEvent) {
switch le {
case webkit2.LoadStarted, webkit2.LoadCommitted, webkit2.LoadRedirected:
// Get current webview URL
curURL := webkit.URI()
// If url is not "amu"
if curURL != "amu" {
// Stop loading
webkit.RunJavaScript("window.stop();", nil)
// Open URL in browser concurrently
go browser.OpenURL(curURL)
// Load base HTML
webkit.LoadHTML(fmt.Sprintf(document, head, ""), "amu")
}
case webkit2.LoadFinished:
// If URL is "amu"
if webkit.URI() == "amu" {
// Load AMU from source buffer
loadAMU(srcBuf, webkit)
}
}
})
// Create new accelerator group
accelGroup, err := NewAccel()
if err != nil {
log.Fatal().Err(err).Msg("Error creating accelerator group")
}
// Set Ctrl+P to print via JavaScript
accelGroup.Add("<Control>p", func() {
webkit.RunJavaScript("window.print();", nil)
})
// Set Ctrl+S to save file
accelGroup.Add("<Control>s", func() {
err := saveFile(window, srcBuf)
if err != nil {
log.Error().Err(err).Msg("Error saving file")
}
})
// Set Ctrl+O to open file
accelGroup.Add("<Control>o", func() {
err := openFile(window, srcBuf)
if err != nil {
log.Error().Err(err).Msg("Error opening file")
}
})
// Add underlying gtk accelerator group to window
window.AddAccelGroup(accelGroup.AccelGroup)
// On window delete event
window.Connect("delete-event", func() bool {
// If source buffer not modified
if !srcBuf.GetModified() {
// Close window
return false
}
// Create confirmation dialog
dlg := gtk.MessageDialogNew(
window,
gtk.DIALOG_MODAL,
gtk.MESSAGE_WARNING,
gtk.BUTTONS_YES_NO,
"Are you sure you want to close?\nYou have unsaved changes.",
)
// Run confirmation dialog and get response
respType := dlg.Run()
dlg.Close()
switch respType {
case gtk.RESPONSE_YES:
return false
case gtk.RESPONSE_NO:
return true
}
return true
})
if len(os.Args) > 1 {
openedFile = os.Args[1]
data, err := ioutil.ReadFile(openedFile)
if err != nil {
log.Fatal().Err(err).Msg("Error opening start file")
}
srcBuf.SetText(string(data))
srcBuf.SetModified(false)
}
window.SetDefaultSize(800, 600)
window.ShowAll()
gtk.Main()
}
func loadAMU(srcBuf *sourceview.SourceBuffer, webkit *webkit2.WebView) {
// Get all text in buffer
src, err := srcBuf.GetText(srcBuf.GetStartIter(), srcBuf.GetEndIter(), true)
if err != nil {
log.Error().Err(err).Msg("Error getting amu source from text view")
return
}
p := parser.New(strings.NewReader(src))
AST, err := p.Parse()
if err != nil {
log.Error().Err(err).Msg("Error parsing amu source")
return
}
formatter := html.NewFormatter(AST, ast.FuncMap{})
// Execute source from buffer
html := formatter.Format()
// Generate full HTML document and encode as JSON for JavaScript
data, err := json.Marshal(html)
if err != nil {
log.Error().Err(err).Msg("Error marshaling string as JSON")
return
}
// Update webview document
webkit.RunJavaScript(fmt.Sprintf(`document.body.innerHTML = %s; renderMathInElement(document.body);`, data), nil)
}