chore (migration): viewerpage editor and audio player

This commit is contained in:
MickaelK
2023-12-30 02:14:12 +11:00
parent 15f630ef99
commit 58cf21d8e1
46 changed files with 1821 additions and 55 deletions
+4 -2
View File
@@ -18,7 +18,7 @@ export default class ComponentMenubar extends window.HTMLElement {
${basename(decodeURIComponent(location.pathname + location.hash)) || " "}
</div>
<div class="action-item no-select">
<div is="component-dropdown"></div>
<!--<div is="component-dropdown"></div>-->
</div>
</span>
</div>
@@ -39,7 +39,9 @@ export default class ComponentMenubar extends window.HTMLElement {
}
render($fragment) {
this.querySelector(".action-item").appendChild($fragment);
const $item = this.querySelector(".action-item");
$item.replaceChildren($fragment);
animate($item, { time: 250, keyframes: slideYIn(2) });
}
}
+69
View File
@@ -0,0 +1,69 @@
class ChromecastManager {
init() {
// TODO: additional rules for setup
let src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
if (document.head.querySelector(`script[src="${src}"]`)) return Promise.resolve();
return new Promise((done) => {
const script = document.createElement("script");
script.src = src;
script.onerror = () => done();
window["__onGCastApiAvailable"] = function(isAvailable) {
if (isAvailable) cast.framework.CastContext.getInstance().setOptions({
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
});
done();
};
document.head.appendChild(script)
});
}
origin() {
return location.origin;
};
isAvailable() {
if (!window.chrome) return false;
else if (!window.chrome.cast) return false;
return window.chrome.cast.isAvailable;
}
// createLink(apiPath) {
// const target = new URL(this.origin() + apiPath);
// const shareID = new window.URL(location.href).searchParams.get("share");
// if (shareID) target.searchParams.append("share", shareID);
// return target.toString();
// }
createRequest(mediaInfo, authorization) {
if (!authorization) Promise.error(new Error("Invalid account"));
// TODO: it would be much much nicer to set the authorization in an HTTP header
// but this would require to create a custom web receiver app, setup accounts on
// google, etc,... Until that happens, we're setting the authorization within the
// url. Once we have that app, the authorisation will come from a customData field
// of a chrome.cast.media.LoadRequest
const target = new URL(mediaInfo.contentId);
target.searchParams.append("authorization", Session.authorization);
mediaInfo.contentId = target.toString();
return new chrome.cast.media.LoadRequest(mediaInfo);
}
context() {
if (!this.isAvailable()) return
return window.cast.framework.CastContext.getInstance();
}
session() {
const context = this.context();
if (!context) return;
return context.getCurrentSession();
}
media() {
const session = this.session();
if (!session) return;
return session.getMediaSession();
}
}
export default new ChromecastManager();
+4
View File
@@ -2,6 +2,10 @@ export function basename(str, sep = "/") {
return str.substr(str.lastIndexOf(sep) + 1);
}
export function extname(str) {
return str.substr(str.lastIndexOf(".") + 1).toLowerCase();
}
export function join(baseURL, segment) {
return new URL(segment, baseURL).pathname;
}
@@ -18,6 +18,26 @@ export function getSelection$() {
return selection$.asObservable();
}
// export function ls() {
// return rxjs.from(new Error("missing cache")).pipe(
// rxjs.catchError(() => rxjs.of({ files: null })),
// rxjs.mergeMap(({ files: filesInCache }) => ajax({
// url: `/api/files/ls?path=${path}`,
// responseType: "json"
// }).pipe(
// rxjs.map(({ responseJSON }) => responseJSON),
// rxjs.filter(({ filesInRemote }) => {
// if (!Array.isArray(filesInCache)) return true;
// if (filesInCache.length != filesInRemote.length) return true;
// for (let i=0; i<filesInCache.length; i++) {
// if (filesInCache[i].name !== filesInRemote[i].name) return true;
// }
// return false;
// }),
// )),
// )
// }
export function ls() {
return rxjs.pipe(
rxjs.mergeMap((path) => ajax({
@@ -4,21 +4,28 @@ import { qs } from "../../lib/dom.js";
import { onDestroy } from "../../lib/skeleton/lifecycle.js";
import { loadCSS, loadJS } from "../../helpers/loader.js";
import { settings_get, settings_put } from "../../lib/settings.js";
import Chromecast from "../../lib/chromecast.js";
import assert from "../../lib/assert.js";
import { basename, extname } from "../../lib/path.js";
import ctrlError from "../ctrl_error.js";
import { render as renderMenubar } from "../../components/menubar.js";
import { menubarDownload, menubarChromecast, buildMenubar } from "./common_menubar.js";
import { ICON } from "./common_icon.js";
import { formatTimecode } from "./common_player.js";
import { transition, getDownloadUrl } from "./common.js";
import { getSession } from "../../model/session.js";
import { get as getConfig } from "../../model/config.js";
import "../../components/menubar.js";
const STATUS_PLAYING = "PLAYING";
const STATUS_PAUSED = "PAUSED";
const STATUS_BUFFERING = "BUFFERING";
export default function(render) {
export default function(render, { mime }) {
const $page = createElement(`
<div class="component_audioplayer">
<component-menubar></component-menubar>
@@ -268,6 +275,9 @@ export default function(render) {
case "KeyL":
setSeek(Math.min(wavesurfer.getDuration(), currentTime(wavesurfer) + 10), wavesurfer);
break;
case "KeyF":
chromecastLoader();
break;
case "KeyJ":
setSeek(Math.max(0, currentTime(wavesurfer) - 10), wavesurfer);
break;
@@ -305,11 +315,70 @@ export default function(render) {
}),
)),
));
// feature9: setup chromecast
effect(ready$.pipe(
rxjs.tap(() => renderMenubar(buildMenubar(
menubarChromecast(),
menubarDownload(),
))),
));
// effect(rxjs.combineLatest(
// setup$,
// getSession(),
// getConfig(),
// ).pipe(
// rxjs.mergeMap(async ([wavesurfer, user, config]) => {
// if (!Chromecast.isAvailable()) return;
// const filename = basename(decodeURIComponent(location.pathname));
// // const link = Chromecast.createLink(getDownloadUrl());
// const media = new chrome.cast.media.MediaInfo(
// getDownloadUrl(),
// mime,
// );
// media.metadata = new chrome.cast.media.MusicTrackMediaMetadata()
// media.metadata.title = "test";
// media.metadata.title = filename.substr(0, filename.lastIndexOf(extname(filename)));
// media.metadata.subtitle = config.name;
// media.metadata.albumName = config.name;
// media.metadata.images = [
// new chrome.cast.Image(origin + "/assets/icons/music.png"),
// ];
// wavesurfer.setMute(true);
// wavesurfer.pause();
// const session = Chromecast.session();
// if (!session) return
// setVolume(session.getVolume() * 100);
// const req = await Chromecast.createRequest(media, user.authorization);
// return session.loadMedia(req);
// // .catch((err) => {
// // console.error(err);
// // notify.send(t("Cannot establish a connection"), "error");
// // setIsChromecast(false);
// // setIsLoading(false);
// // });
// }),
// ));
}
export function init() {
return Promise.all([
setup_chromecast(),
loadJS(import.meta.url, "../../lib/vendor/wavesurfer.js"),
loadCSS(import.meta.url, "./application_audio.css"),
]);
}
function setup_chromecast() {
if (!("chrome" in window)) {
return Promise.resolve();
} else if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
return Promise.resolve();
}
// if (!CONFIG.enable_chromecast) {
// return Promise.resolve();
// } else
return Chromecast.init();
}
@@ -1,45 +1,334 @@
.component_editor {
overflow-y: auto;
flex-direction: column;
flex: 1;
display: flex;
flex-grow: 1;
min-height: 0;
width: 100%;
}
.component_editor > div {
width: 100%;
position: relative;
}
.component_editor > div #editor {
width: 100%;
position: absolute;
left: 0;
top: 0;
bottom: 0;
}
.CodeMirror {
width: 100%;
height: 100%;
color: #3b4045;
background: var(--bg-color);
}
.CodeMirror-sizer > div {
padding-top: 4px;
padding-bottom: 5px;
}
.CodeMirror-scroll {
-webkit-overflow-scrolling: touch;
}
.CodeMirror-foldmarker {
padding: 5px;
}
/* HIDE LINE NUMBERS ON MOBILE */
@media screen and (max-width: 400px) {
.CodeMirror-sizer {
margin-left: 0 !important;
}
.CodeMirror-gutters {
display: none;
}
.CodeMirror-gutter-wrapper {
display: none;
}
}
.CodeMirror-linenumber {
cursor: pointer;
}
/* SEARCH */
.CodeMirror-dialog {
position: fixed;
left: 0;
right: 0;
background: #525659;
z-index: 15;
padding: 5px .8em;
overflow: hidden;
color: #e2e2e2;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
}
.CodeMirror-dialog-top {
border-bottom: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog-bottom {
border-top: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog input {
border: none;
outline: none;
background: transparent;
width: 20em;
color: white;
}
.CodeMirror-dialog button {
font-size: 70%;
}
/* Font stuff */
.CodeMirror {
font-size: 16px;
font-family: "Source Code Pro", monospace;
font-family: 'Source Code Pro', monospace;
}
.CodeMirror-scroll { -webkit-overflow-scrolling: touch; }
.cm-s-default .cm-header {
font-size: 18px;
}
@font-face {
font-family: "Source Code Pro";
font-style: normal;
font-weight: 400;
src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
.cm-s-default .cm-header.cm-level1 {
font-size: 19px;
}
@font-face {
font-family: "Source Code Pro";
font-style: normal;
font-weight: 400;
src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
@media only screen and (max-width: 600px) {
.CodeMirror {
font-size: 14px;
}
.cm-s-default .cm-header {
font-size: 15px;
}
.cm-s-default .cm-header.cm-level1 {
font-size: 16px;
}
}
@font-face {
font-family: "Source Code Pro";
font-style: normal;
/* Make things more confy */
.CodeMirror .CodeMirror-code {
line-height: 1.3em;
}
.CodeMirror .CodeMirror-code > div {
clear: both;
}
.CodeMirror[mode="orgmode"] .CodeMirror-code {
line-height: 1.5em;
}
.CodeMirror .CodeMirror-line {
padding-left: 10%;
padding-right: 0;
max-width: 950px;
}
@media screen and (max-width: 1150px) {
.CodeMirror .CodeMirror-line {
padding-left: 8%;
padding-right: 0;
}
}
@media screen and (max-width: 900px) {
.CodeMirror .CodeMirror-line {
padding-left: 5%;
padding-right: 0;
}
}
@media screen and (max-width: 800px) {
.CodeMirror .CodeMirror-line {
padding-left: 2%;
padding-right: 0;
}
}
.CodeMirror .CodeMirror-linenumber {
color: var(--color);
opacity: 0.6;
}
.CodeMirror .CodeMirror-gutters {
box-shadow: none;
background-color: inherit;
border-right: none;
}
/* Widget stuff */
.CodeMirror-linewidget img {
cursor: pointer;
margin: 10px;
height: 300px;
max-width: 80%;
text-align: center;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
background: var(--dark);
}
/* Code Highlight Theme */
.cm-s-default .cm-header {
color: #3E7AA6;
line-height: 1em;
font-weight: 600;
src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Source Code Pro";
font-style: normal;
font-weight: 600;
src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
.cm-header.cm-level1 {
color: #376e95;
}
.cm-s-default .cm-keyword {
color: var(--emphasis-secondary);
}
.cm-s-default .cm-header.cm-org-level-star {
color: #6f6f6f;
vertical-align: baseline;
display: inline-block;
padding-left: 5px;
margin-left: -5px;
cursor: pointer;
}
.cm-s-default .cm-header.cm-org-todo {
color: #FF8355;
font-weight: normal;
cursor: pointer;
}
.cm-s-default .cm-header.cm-org-done {
color: #3BB27C;
font-weight: normal;
cursor: pointer;
}
.cm-s-default .cm-header.cm-org-priority {
cursor: pointer;
font-weight: normal;
}
.cm-s-default .cm-org-toggle {
cursor: pointer;
background: var(--light);
color: var(--super-light);
border-radius: 3px;
font-weight: bold;
padding-bottom: 2px;
vertical-align: text-bottom;
opacity: 0.7;
}
.cm-s-default .cm-void {
display: inline-block;
max-width: 10px;
overflow: hidden;
white-space: nowrap;
}
.cm-s-default .cm-header.cm-comment {
font-weight: normal;
font-size: 0.9em !important;
float: right;
display: inline-block;
color: var(--emphasis);
line-height: 23px;
}
pre.CodeMirror-line {
clear: right;
}
.cm-s-default .cm-link {
color: var(--emphasis);
}
.cm-s-default .cm-strong {
color: var(--emphasis);
}
.cm-s-default .cm-org-url, .cm-s-default .cm-org-image {
color: var(--emphasis) !important;
border-bottom: 1px dashed var(--light);
cursor: pointer;
}
.cm-s-default .cm-variable-3 {
color: #085;
}
.cm-s-default .cm-comment {
color: var(--light);
}
.cm-s-default .cm-string, .cm-s-default .cm-string-2 {
color: #c41a16;
}
.cm-s-default .cm-def {
color: #445588;
}
.cm-s-default .cm-quote {
color: var(--dark);
}
.CodeMirror-gutters {
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.1);
}
.CodeMirror-foldmarker {
padding-left: 5px;
color: var(--color);
text-shadow: 1px 1px 10px var(--color);
}
span.CodeMirror-matchingbracket,
span.CodeMirror-matchingtag {
background: rgba(0, 0, 0, 0.1);
color: inherit !important;
}
/* BUGFIX */
.CodeMirror-cursor {
min-width: 1px !important;
}
/* DAR MODE THEME */
.dark-mode .CodeMirror {
background: #242424;
}
.dark-mode .CodeMirror, .dark-mode .CodeMirror .CodeMirror-linenumber, .dark-mode .CodeMirror .cm-variable-2, .dark-mode .CodeMirror .cm-variable-3, .dark-mode .CodeMirror .cm-number, .dark-mode .CodeMirror .cm-quote {
color: #f6f3e8;
}
.dark-mode .CodeMirror .cm-string, .dark-mode .CodeMirror .cm-string-2 {
color: #95e454;
}
.dark-mode .CodeMirror .cm-atom {
color: #e5786d;
}
.dark-mode .CodeMirror .cm-keyword, .dark-mode .CodeMirror .cm-meta, .dark-mode .CodeMirror .cm-header, .dark-mode .CodeMirror .cm-property {
color: #8ac6f2;
}
.dark-mode .CodeMirror .cm-def, .dark-mode .CodeMirror .cm-tag, .dark-mode .CodeMirror .cm-attribute, .dark-mode .CodeMirror .cm-builtin, .dark-mode .CodeMirror .cm-qualifier {
color: #cae682;
}
.dark-mode .CodeMirror .cm-comment {
color: #99968b;
}
.dark-mode .CodeMirror .CodeMirror-cursor {
background-color: #99968b;
}
.dark-mode .CodeMirror .CodeMirror-selected {
background-color: rgba(0, 0, 0, 0.3);
}
.dark-mode .CodeMirror .cm-error {
color: var(--error);
}
.dark-mode .CodeMirror .cm-org-url, .dark-mode .CodeMirror .cm-link, .dark-mode .CodeMirror .cm-org-image, .dark-mode .CodeMirror .cm-url {
color: #ccaa8f !important;
}
@@ -1,15 +1,22 @@
import { createElement } from "../../lib/skeleton/index.js";
import { createElement, onDestroy } from "../../lib/skeleton/index.js";
import rxjs, { effect } from "../../lib/rx.js";
import { animate, slideXIn, opacityOut } from "../../lib/animate.js";
import { qs } from "../../lib/dom.js";
import { createLoader } from "../../components/loader.js";
import { loadCSS, loadJS } from "../../helpers/loader.js";
import ajax from "../../lib/ajax.js";
import { extname } from "../../lib/path.js";
import { get as getConfig } from "../../model/config.js";
import ctrlError from "../ctrl_error.js";
import ctrlDownloader, { init as initDownloader } from "./application_downloader.js";
import { transition, getDownloadUrl } from "./common.js";
import { getFile$, saveFile$, transition, getFilename, getCurrentPath } from "./common.js";
import { $ICON } from "./common_fab.js";
import { fileOptions } from "./model_files.js";
import "../../components/menubar.js";
import "../../components/fab.js";
import "../../components/icon.js";
const TIME_BEFORE_ABORT_EDIT = 5000;
@@ -18,20 +25,21 @@ export default async function(render) {
<div class="component_ide">
<component-menubar class="hidden"></component-menubar>
<div class="component_editor hidden"></div>
<button is="component-fab" class="hidden"></button>
</div>
`);
render($page);
const $editor = qs($page, ".component_editor");
const $menubar = qs($page, "component-menubar");
const content$ = ajax(getDownloadUrl()).pipe(
rxjs.map(({ response }) => response),
rxjs.shareReplay(),
);
const $fab = qs($page, `[is="component-fab"]`);
const getConfig$ = getConfig().pipe(rxjs.shareReplay(1));
const content$ = new rxjs.ReplaySubject(1);
// feature1: setup the dom
const removeLoader = createLoader($page);
effect(rxjs.race(
ajax(getDownloadUrl()).pipe(rxjs.map(({ response }) => response)),
const setup$ = rxjs.race(
getFile$(),
ajax("/about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)),
).pipe(
rxjs.mergeMap((content) => {
@@ -46,34 +54,104 @@ export default async function(render) {
}
return rxjs.of(content);
}),
rxjs.mergeMap((content) => getConfig$.pipe(
rxjs.mergeMap((config) => rxjs.from(loadKeybinding(config.editor)).pipe(rxjs.mapTo(config))),
rxjs.map((config) => [content, config]),
rxjs.mergeMap((arr) => rxjs.from(loadMode(extname(getFilename()))).pipe(
rxjs.map((mode) => arr.concat([mode])),
)),
rxjs.mergeMap((arr) => fileOptions(getCurrentPath()).pipe(
rxjs.map((acl) => arr.concat([acl])),
)),
)),
removeLoader,
rxjs.tap((content) => {
rxjs.map(([content, config, mode, acl]) => {
content$.next(content);
$editor.classList.remove("hidden");
window.CodeMirror($editor, {
const editor = window.CodeMirror($editor, {
value: content,
lineNumbers: true,
// mode: mode,
// keyMap: ["emacs", "vim"].indexOf(CONFIG["editor"]) === -1 ?
// "sublime" : CONFIG["editor"],
mode,
keyMap: ["emacs", "vim"].indexOf(config["editor"]) === -1 ? "sublime" : config["editor"],
lineWrapping: true,
// readOnly: !this.props.readonly,
foldOptions: {
widget: "...",
},
readOnly: !/PUT/.test(acl),
foldOptions: { widget: "..." },
matchBrackets: {},
autoCloseBrackets: true,
matchTags: { bothTags: true },
autoCloseTags: true,
});
transition($editor);
editor.getWrapperElement().setAttribute("mode", mode);
if (!("ontouchstart" in window)) editor.focus();
if (config["editor"] === "emacs") editor.addKeyMap({
"Ctrl-X Ctrl-C": (cm) => window.history.back(),
});
onDestroy(() => editor.clearHistory());
$menubar.classList.remove("hidden");
editor.execCommand("save");
return editor;
}),
rxjs.tap((editor) => requestAnimationFrame(() => editor.refresh())),
rxjs.share(),
rxjs.catchError(ctrlError()),
);
effect(setup$);
// feature2: handle resize
effect(setup$.pipe(
rxjs.mergeMap((editor) => rxjs.fromEvent(window, "resize").pipe(
rxjs.tap(() => editor.refresh()),
)),
));
// feature3: handle UI for edit
effect(setup$.pipe(
rxjs.switchMap((editor) => new rxjs.Observable((observer) => editor.on("change", (cm) => observer.next(cm)))),
rxjs.mergeMap((editor) => content$.pipe(rxjs.map((oldContent) => [editor, editor.getValue(), oldContent]))),
rxjs.tap(async ([editor, newContent = "", oldContent = ""]) => {
if ($fab.disabled) return;
else if (newContent === oldContent) {
await animate($fab, { time: 100, keyframes: opacityOut() });
$fab.classList.add("hidden");
return
}
const shouldAnimate = $fab.classList.contains("hidden");
$fab.classList.remove("hidden");
$fab.render($ICON.SAVING);
$fab.onclick = () => CodeMirror.commands.save(editor);
// TODO: breadcrumb saving hint *
if (shouldAnimate) await animate($fab, { time: 100, keyframes: slideXIn(40) });
}),
));
// feature4: save
effect(setup$.pipe(
rxjs.mergeMap((editor) => new rxjs.Observable((observer) => {
CodeMirror.commands.save = (cm) => observer.next(cm);
})),
rxjs.mergeMap((cm) => {
$fab.render($ICON.LOADING)
$fab.disabled = true;
return rxjs.of(cm.getValue()).pipe(
saveFile$(),
rxjs.tap((content) => {
$fab.removeAttribute("disabled");
content$.next(content);
}),
);
}),
rxjs.catchError(ctrlError()),
))
));
}
function has_binary(str) {
return /\ufffd/.test(str);
let countUnrepresentableChar = 0;
for (let i=0; i<str.length; i++) {
if (countUnrepresentableChar > 2) return true;
else if (str[i] === "\ufffd") countUnrepresentableChar += 1;
}
return false;
}
export function init() {
@@ -81,5 +159,88 @@ export function init() {
loadCSS(import.meta.url, "../../lib/vendor/codemirror/lib/codemirror.css"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/lib/codemirror.js"),
loadCSS(import.meta.url, "./application_editor.css"),
]);
]).then(() => Promise.all([
loadJS(import.meta.url, "../../lib/vendor/codemirror/keymap/emacs.js"),
// search
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/search/searchcursor.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/search/search.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/comment/comment.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/dialog/dialog.js"),
// folding
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldcode.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldgutter.js"),
loadCSS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/foldgutter.css"),
// editing feature
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/matchbrackets.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/closebrackets.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/closetag.js"),
]));
}
function loadMode(ext) {
let mode = "text";
let before = Promise.resolve();
if (ext === "org" || ext === "org_archive") {
mode = "orgmode";
before = loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/fold/xml-fold.js").then(() => loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/edit/matchtags.js"));
} else if (ext === "sh") mode = "shell";
else if (ext === "py") mode = "python";
else if (ext === "html" || ext === "htm") {
mode = "htmlmixed";
before = Promise.all([
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/xml/xml.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/javascript/javascript.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/css/css.js"),
]);
} else if (ext === "css") mode = "css";
else if (ext === "less" || ext === "scss" || ext === "sass") mode = "sass";
else if (ext === "js" || ext === "json") mode = "javascript";
else if (ext === "jsx") mode = "jsx";
else if (ext === "php" || ext === "php5" || ext === "php4") mode = "php";
else if (ext === "elm") mode = "elm";
else if (ext === "erl") mode = "erlang";
else if (ext === "go") mode = "go";
else if (ext === "markdown" || ext === "md") {
mode = "yaml-frontmatter";
before = Promise.all([
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/markdown/markdown.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/gfm/gfm.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/mode/yaml/yaml.js"),
loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/mode/overlay.js"),
]);
} else if (ext === "pl" || ext === "pm") mode = "perl";
else if (ext === "clj") mode = "clojure";
else if (ext === "el" || ext === "lisp" || ext === "cl" ||
ext === "emacs") mode = "commonlisp";
else if (ext === "dockerfile") {
mode = "dockerfile";
before = loadJS(import.meta.url, "../../lib/vendor/codemirror/addon/mode/simple.js");
} else if (ext === "R") mode = "r";
else if (ext === "makefile") mode = "cmake";
else if (ext === "rb") mode = "ruby";
else if (ext === "sql") mode = "sql";
else if (ext === "xml" || ext === "rss" || ext === "svg" ||
ext === "atom") mode = "xml";
else if (ext === "yml" || ext === "yaml") mode = "yaml";
else if (ext === "lua") mode = "lua";
else if (ext === "csv") mode = "spreadsheet";
else if (ext === "rs" || ext === "rlib") mode = "rust";
else if (ext === "latex" || ext === "tex") mode = "stex";
else if (ext === "diff" || ext === "patch") mode = "diff";
else if (ext === "sparql") mode = "sparql";
else if (ext === "properties") mode = "properties";
else if (ext === "c" || ext === "cpp" || ext === "java" ||
ext === "h") mode = "clike";
return before.then(() => loadJS(import.meta.url, `./application_editor/${mode}.js`, { type: "module" }))
.catch(() => loadJS(import.meta.url, "./application_editor/text.js", { type: "module" }))
.then((module) => Promise.resolve(mode));
}
function loadKeybinding(editor) {
if (editor === "emacs" || !editor) {
return Promise.resolve();
}
return loadJS(import.meta.url, `./application_editor/keymap_${editor}.js`, { type: "module" });
}
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/clike/clike.js";
CodeMirror.__mode = "clike";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/clojure/clojure.js";
CodeMirror.__mode = "clojure";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/cmake/cmake.js";
CodeMirror.__mode = "cmake";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/commonlisp/commonlisp.js";
CodeMirror.__mode = "commonlisp";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/css/css.js";
CodeMirror.__mode = "css";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/diff/diff.js";
CodeMirror.__mode = "diff";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/dockerfile/dockerfile.js";
CodeMirror.__mode = "dockerfile";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/elm/elm.js";
CodeMirror.__mode = "elm";
export default CodeMirror;
@@ -0,0 +1,728 @@
export const org_cycle = (cm) => {
const pos = cm.getCursor();
isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
};
const state = {
stab: "CONTENT",
};
export const org_set_fold = (cm) => {
const cursor = cm.getCursor();
set_folding_mode(cm, state.stab);
cm.setCursor(cursor);
return state.stab;
};
/*
* DONE: Global visibility cycling
* TODO: or move to previous table field.
*/
export const org_shifttab = (cm) => {
if (state.stab === "SHOW_ALL") {
state.stab = "OVERVIEW";
} else if (state.stab === "OVERVIEW") {
state.stab = "CONTENT";
} else if (state.stab === "CONTENT") {
state.stab = "SHOW_ALL";
}
set_folding_mode(cm, state.stab);
return state.stab;
};
function set_folding_mode(cm, mode) {
if (mode === "OVERVIEW") {
folding_mode_overview(cm);
} else if (mode === "SHOW_ALL") {
folding_mode_all(cm);
} else if (mode === "CONTENT") {
folding_mode_content(cm);
}
cm.refresh();
function folding_mode_overview(cm) {
cm.operation(function() {
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
fold(cm, CodeMirror.Pos(i, 0));
}
});
}
function folding_mode_content(cm) {
cm.operation(function() {
let previous_header = null;
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
fold(cm, CodeMirror.Pos(i, 0));
if (/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true) {
const level = cm.getLine(i).replace(/^(\*+).*/, "$1").length;
if (previous_header && level > previous_header.level) {
unfold(cm, CodeMirror.Pos(previous_header.line, 0));
}
previous_header = {
line: i,
level: level,
};
}
}
});
}
function folding_mode_all(cm) {
cm.operation(function() {
for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
if (/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true) {
unfold(cm, CodeMirror.Pos(i, 0));
}
}
});
}
}
/*
* Promote heading or move table column to left.
*/
export const org_metaleft = (cm) => {
const line = cm.getCursor().line;
_metaleft(cm, line);
};
function _metaleft(cm, line) {
let p = null;
if (p = isTitle(cm, line)) {
if (p["level"] > 1) {
cm.replaceRange(
"",
{ line: p.start, ch: 0 },
{ line: p.start, ch: 1 },
);
}
} else if (p = isItemList(cm, line)) {
for (let i=p.start; i<=p.end; i++) {
if (p["level"] > 0) {
cm.replaceRange(
"",
{ line: i, ch: 0 },
{ line: i, ch: 2 },
);
}
}
} else if (p = isNumberedList(cm, line)) {
for (let i=p.start; i<=p.end; i++) {
if (p["level"] > 0) {
cm.replaceRange(
"",
{ line: i, ch: 0 },
{ line: i, ch: 3 },
);
}
}
rearrange_list(cm, line);
}
}
/*
* Demote a subtree, a list item or move table column to right.
* In front of a drawer or a block keyword, indent it correctly.
*/
export const org_metaright = (cm) => {
const line = cm.getCursor().line;
_metaright(cm, line);
};
function _metaright(cm, line) {
let p = null;
let tmp = null;
if (p = isTitle(cm, line)) {
cm.replaceRange("*", { line: p.start, ch: 0 });
} else if (p = isItemList(cm, line)) {
if (tmp = isItemList(cm, p.start - 1)) {
if (p.level < tmp.level + 1) {
for (let i=p.start; i<=p.end; i++) {
cm.replaceRange(" ", { line: i, ch: 0 });
}
}
}
} else if (p = isNumberedList(cm, line)) {
if (tmp = isNumberedList(cm, p.start - 1)) {
if (p.level < tmp.level + 1) {
for (let i=p.start; i<=p.end; i++) {
cm.replaceRange(" ", { line: i, ch: 0 });
}
rearrange_list(cm, p.start);
}
}
}
}
/*
* Insert a new heading or wrap a region in a table
*/
export const org_meta_return = (cm) => {
const line = cm.getCursor().line;
const content = cm.getLine(line);
let p = null;
if (p = isItemList(cm, line)) {
const level = p.level;
cm.replaceRange(
"\n"+" ".repeat(level*2)+"- ",
{ line: p.end, ch: cm.getLine(p.end).length },
);
cm.setCursor({ line: p.end+1, ch: level*2+2 });
} else if (p = isNumberedList(cm, line)) {
const level = p.level;
cm.replaceRange(
"\n"+" ".repeat(level*3)+(p.n+1)+". ",
{ line: p.end, ch: cm.getLine(p.end).length },
);
cm.setCursor({ line: p.end+1, ch: level*3+3 });
rearrange_list(cm, line);
} else if (p = isTitle(cm, line)) {
const tmp = previousOfType(cm, "title", line);
const level = tmp && tmp.level || 1;
cm.replaceRange("\n"+"*".repeat(level)+" ", { line: line, ch: content.length });
cm.setCursor({ line: line+1, ch: level+1 });
} else if (content.trim() === "") {
cm.replaceRange("* ", { line: line, ch: 0 });
cm.setCursor({ line: line, ch: 2 });
} else {
cm.replaceRange("\n\n* ", { line: line, ch: content.length });
cm.setCursor({ line: line + 2, ch: 2 });
}
};
const TODO_CYCLES = ["TODO", "DONE", ""];
/*
* Cycle the thing at point or in the current line, depending on context.
* Depending on context, this does one of the following:
* - TODO: switch a timestamp at point one day into the past
* - DONE: on a headline, switch to the previous TODO keyword.
* - TODO: on an item, switch entire list to the previous bullet type
* - TODO: on a property line, switch to the previous allowed value
* - TODO: on a clocktable definition line, move time block into the past
*/
export const org_shiftleft = (cm) => {
const cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1));
const line = cm.getCursor().line;
const content = cm.getLine(line);
const params = isTitle(cm, line);
if (params === null) return;
params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
cm.replaceRange(
makeTitle(params),
{ line: line, ch: 0 },
{ line: line, ch: content.length },
);
};
/*
* Cycle the thing at point or in the current line, depending on context.
* Depending on context, this does one of the following:
* - TODO: switch a timestamp at point one day into the future
* - DONE: on a headline, switch to the next TODO keyword.
* - TODO: on an item, switch entire list to the next bullet type
* - TODO: on a property line, switch to the next allowed value
* - TODO: on a clocktable definition line, move time block into the future
*/
export const org_shiftright = (cm) => {
cm.operation(() => {
const cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]);
const line = cm.getCursor().line;
const content = cm.getLine(line);
const params = isTitle(cm, line);
if (params === null) return;
params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
cm.replaceRange(
makeTitle(params),
{ line: line, ch: 0 },
{ line: line, ch: content.length },
);
});
};
export const org_insert_todo_heading = (cm) => {
cm.operation(() => {
const line = cm.getCursor().line;
const content = cm.getLine(line);
let p = null;
if (p = isItemList(cm, line)) {
const level = p.level;
cm.replaceRange(
"\n"+" ".repeat(level*2)+"- [ ] ",
{ line: p.end, ch: cm.getLine(p.end).length },
);
cm.setCursor({ line: line+1, ch: 6+level*2 });
} else if (p = isNumberedList(cm, line)) {
const level = p.level;
cm.replaceRange(
"\n"+" ".repeat(level*3)+(p.n+1)+". [ ] ",
{ line: p.end, ch: cm.getLine(p.end).length },
);
cm.setCursor({ line: p.end+1, ch: level*3+7 });
rearrange_list(cm, line);
} else if (p = isTitle(cm, line)) {
const level = p && p.level || 1;
cm.replaceRange("\n"+"*".repeat(level)+" TODO ", { line: line, ch: content.length });
cm.setCursor({ line: line+1, ch: level+6 });
} else if (content.trim() === "") {
cm.replaceRange("* TODO ", { line: line, ch: 0 });
cm.setCursor({ line: line, ch: 7 });
} else {
cm.replaceRange("\n\n* TODO ", { line: line, ch: content.length });
cm.setCursor({ line: line + 2, ch: 7 });
}
});
};
/*
* Move subtree up or move table row up.
* Calls org-move-subtree-up or org-table-move-row or
* org-move-item-up, depending on context
*/
export const org_metaup = (cm) => {
cm.operation(() => {
const line = cm.getCursor().line;
let p = null;
if (p = isItemList(cm, line)) {
const a = isItemList(cm, p.start - 1);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
rearrange_list(cm, line);
}
} else if (p = isNumberedList(cm, line)) {
const a = isNumberedList(cm, p.start - 1);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
rearrange_list(cm, line);
}
} else if (p = isTitle(cm, line)) {
let _line = line;
let a;
do {
_line -= 1;
if (a = isTitle(cm, _line, p.level)) {
break;
}
} while (_line > 0);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
org_set_fold(cm);
}
}
});
};
/*
* Move subtree down or move table row down.
* Calls org-move-subtree-down or org-table-move-row or
* org-move-item-down, depending on context
*/
export const org_metadown = (cm) => {
cm.operation(() => {
const line = cm.getCursor().line;
let p = null;
if (p = isItemList(cm, line)) {
const a = isItemList(cm, p.end + 1);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
}
} else if (p = isNumberedList(cm, line)) {
const a = isNumberedList(cm, p.end + 1);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
}
rearrange_list(cm, line);
} else if (p = isTitle(cm, line)) {
const a = isTitle(cm, p.end + 1, p.level);
if (a) {
swap(cm, [p.start, p.end], [a.start, a.end]);
org_set_fold(cm);
}
}
});
};
export const org_shiftmetaright = function(cm) {
cm.operation(() => {
const line = cm.getCursor().line;
let p = null;
if (p = isTitle(cm, line)) {
_metaright(cm, line);
for (let i=p.start + 1; i<=p.end; i++) {
if (isTitle(cm, i)) {
_metaright(cm, i);
}
}
}
});
};
export const org_shiftmetaleft = function(cm) {
cm.operation(() => {
const line = cm.getCursor().line;
let p = null;
if (p = isTitle(cm, line)) {
if (p.level === 1) return;
_metaleft(cm, line);
for (let i=p.start + 1; i<=p.end; i++) {
if (isTitle(cm, i)) {
_metaleft(cm, i);
}
}
}
});
};
function makeTitle(p) {
let content = "*".repeat(p["level"])+" ";
if (p["status"]) {
content += p["status"]+" ";
}
content += p["content"];
return content;
}
function previousOfType(cm, type, line) {
let tmp;
let i;
for (i=line - 1; i>0; i--) {
if (type === "list" || type === null) {
tmp = isItemList(cm, line);
} else if (type === "numbered" || type === null) {
tmp = isNumberedList(cm, line);
} else if (type === "title" || type === null) {
tmp = isTitle(cm, line);
}
if (tmp !== null) {
return tmp;
}
}
return null;
}
function isItemList(cm, line) {
const rootLineItem = findRootLine(cm, line);
if (rootLineItem === null) return null;
line = rootLineItem;
const content = cm.getLine(line);
if (content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null;
const padding = content.replace(/^(\s*).*$/, "$1").length;
if (padding % 2 !== 0) return null;
return {
type: "list",
level: padding / 2,
content: content.trimLeft().replace(/^\s*\-\s(.*)$/, "$1"),
start: line,
end: function(_cm, _line) {
let line_candidate = _line;
let content = null;
do {
_line += 1;
content = _cm.getLine(_line);
if (content === undefined || content.trimLeft()[0] === "-") {
break;
} else if (/^\s+/.test(content)) {
line_candidate = _line;
continue;
} else {
break;
}
} while (_line <= _cm.lineCount());
return line_candidate;
}(cm, line),
};
function findRootLine(_cm, _line) {
let content;
do {
content = _cm.getLine(_line);
if (/^\s*\-/.test(content)) return _line;
else if (/^\s+/.test(content) === false) {
break;
}
_line -= 1;
} while (_line >= 0);
return null;
}
}
function isNumberedList(cm, line) {
const rootLineItem = findRootLine(cm, line);
if (rootLineItem === null) return null;
line = rootLineItem;
const content = cm.getLine(line);
if (/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null;
const padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length;
if (padding % 3 !== 0) return null;
return {
type: "numbered",
level: padding / 3,
content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, "$1"),
start: line,
end: function(_cm, _line) {
let line_candidate = _line;
let content = null;
do {
_line += 1;
content = _cm.getLine(_line);
if (content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())) {
break;
} else if (/^\s+/.test(content)) {
line_candidate = _line;
continue;
} else {
break;
}
} while (_line <= _cm.lineCount());
return line_candidate;
}(cm, line),
// specific
n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")),
separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, "$1"),
};
function findRootLine(_cm, _line) {
let content;
do {
content = _cm.getLine(_line);
if (/^\s*[0-9]+[\.\)]\s/.test(content)) return _line;
else if (/^\s+/.test(content) === false) {
break;
}
_line -= 1;
} while (_line >= 0);
return null;
}
}
function isTitle(cm, line, level) {
const content = cm.getLine(line);
if (/^\*+\s/.test(content) === false) return null;
const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
const reference_level = match[1].length;
if (level !== undefined && level !== reference_level) return null;
if (match === null) return null;
return {
type: "title",
level: reference_level,
content: match[3],
start: line,
end: function(_cm, _line) {
let line_candidate = _line;
let content = null;
do {
_line += 1;
content = _cm.getLine(_line);
if (content === undefined) break;
const match = content.match(/^(\*+)\s.*/);
if (
match && match[1] &&
( match[1].length === reference_level || match[1].length < reference_level)
) {
break;
} else {
line_candidate = _line;
continue;
}
} while (_line <= _cm.lineCount());
return line_candidate;
}(cm, line),
// specific
status: match[2].trim(),
};
}
function rearrange_list(cm, line) {
const line_inferior = find_limit_inferior(cm, line);
const line_superior = find_limit_superior(cm, line);
let last_p = null;
let p;
for (let i=line_inferior; i<=line_superior; i++) {
if (p = isNumberedList(cm, i)) {
// rearrange numbers on the numbered list
if (last_p) {
if (p.level === last_p.level) {
const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
} else if (p.level > last_p.level) {
if (p.n !== 1) {
setNumber(cm, p.start, 1);
}
} else if (p.level < last_p.level) {
const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
}
} else {
if (p.n !== 1) setNumber(cm, p.start, 1);
}
}
if (p = (isNumberedList(cm, i) || isItemList(cm, i))) {
// rearrange spacing levels in list
if (last_p) {
if (p.level > last_p.level) {
if (p.level !== last_p.level + 1) {
setLevel(cm, [p.start, p.end], last_p.level + 1, p.type);
}
}
} else {
if (p.level !== 0) {
setLevel(cm, [p.start, p.end], 0, p.type);
}
}
}
last_p = p;
// we can process content block instead of line
if (p) {
i += (p.end - p.start);
}
}
function findLastAtLevel(_cm, line, line_limit_inf, level) {
let p;
do {
line -= 1;
if ((p = isNumberedList(_cm, line)) && p.level === level) {
return p;
}
} while (line > line_limit_inf);
return null;
}
function setLevel(_cm, range, level, type) {
let content;
let i;
for (i=range[0]; i<=range[1]; i++) {
content = cm.getLine(i).trimLeft();
const n_spaces = function(_level, _line, _type) {
let spaces = _level * 3;
if (_line > 0) {
spaces += _type === "numbered" ? 3 : 2;
}
return spaces;
}(level, i - range[0], type);
content = " ".repeat(n_spaces) + content;
cm.replaceRange(
content,
{ line: i, ch: 0 },
{ line: i, ch: _cm.getLine(i).length },
);
}
}
function setNumber(_cm, line, level) {
const content = _cm.getLine(line);
const new_content = content.replace(/[0-9]+\./, level+".");
cm.replaceRange(
new_content,
{ line: line, ch: 0 },
{ line: line, ch: content.length },
);
}
function find_limit_inferior(_cm, _line) {
let content;
let p;
let match;
let line_candidate = _line;
do {
content = _cm.getLine(_line);
p = isNumberedList(_cm, _line);
match = /(\s+).*$/.exec(content);
if (p) line_candidate = _line;
if (!p || !match) break;
_line -= 1;
} while (_line >= 0);
return line_candidate;
}
function find_limit_superior(_cm, _line) {
let content;
let p;
let match;
let line_candidate = _line;
do {
content = _cm.getLine(_line);
p = isNumberedList(_cm, _line);
match = /(\s+).*$/.exec(content);
if (p) line_candidate = _line;
if (!p || !match) break;
_line += 1;
} while (_line < _cm.lineCount());
return line_candidate;
}
}
function swap(cm, from, to) {
const from_content = cm.getRange(
{ line: from[0], ch: 0 },
{ line: from[1], ch: cm.getLine(from[1]).length },
);
const to_content = cm.getRange(
{ line: to[0], ch: 0 },
{ line: to[1], ch: cm.getLine(to[1]).length },
);
const cursor = cm.getCursor();
if (to[0] > from[0]) {
// moving down
cm.replaceRange(
from_content,
{ line: to[0], ch: 0 },
{ line: to[1], ch: cm.getLine(to[1]).length },
);
cm.replaceRange(
to_content,
{ line: from[0], ch: 0 },
{ line: from[1], ch: cm.getLine(from[1]).length },
);
cm.setCursor({
line: cursor.line + (to[1] - to[0] + 1),
ch: cursor.ch,
});
} else {
// moving up
cm.replaceRange(
to_content,
{ line: from[0], ch: 0 },
{ line: from[1], ch: cm.getLine(from[1]).length },
);
cm.replaceRange(
from_content,
{ line: to[0], ch: 0 },
{ line: to[1], ch: cm.getLine(to[1]).length },
);
cm.setCursor({
line: cursor.line - (to[1] - to[0] + 1),
ch: cursor.ch,
});
}
}
export function fold(cm, start) {
cm.foldCode(start, null, "fold");
}
export function unfold(cm, start) {
cm.foldCode(start, null, "unfold");
}
export function isFold(cm, start) {
const line = start.line;
const marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0));
for (let i = 0; i < marks.length; ++i) {
if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
}
return false;
}
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/erlang/erlang.js";
CodeMirror.__mode = "erlang";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/go/go.js";
CodeMirror.__mode = "go";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/htmlmixed/htmlmixed.js";
CodeMirror.__mode = "htmlmixed";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/javascript/javascript.js";
CodeMirror.__mode = "javascript";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/jsx/jsx.js";
CodeMirror.__mode = "jsx";
export default CodeMirror;
@@ -0,0 +1 @@
import "../../../lib/vendor/codemirror/keymap/sublime.js";
@@ -0,0 +1 @@
import "../../../lib/vendor/codemirror/keymap/vim.js";
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/lua/lua.js";
CodeMirror.__mode = "lua";
export default CodeMirror;
@@ -0,0 +1,313 @@
import "../../../lib/vendor/codemirror/addon/mode/simple.js";
import {
org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup,
org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold,
isFold, org_set_fold, org_shiftmetaleft, org_shiftmetaright,
} from "./emacs-org.js";
// TODO:
// import { pathBuilder, dirname, currentShare } from "../../../helpers/";
const CodeMirror = window.CodeMirror;
CodeMirror.__mode = "orgmode";
CodeMirror.defineSimpleMode("orgmode", {
start: [
{ regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star", "header level1 org-todo", "header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"] },
{ regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star", "header org-todo", "header org-done", "header org-priority", "header", "header void", "header comment"] },
{ regex: /(\+[^\+]+\+)/, token: ["strikethrough"] },
{ regex: /(\*[^\*]+\*)/, token: ["strong"] },
{ regex: /(\/[^\/]+\/)/, token: ["em"] },
{ regex: /(\_[^\_]+\_)/, token: ["link"] },
{ regex: /(\~[^\~]+\~)/, token: ["comment"] },
{ regex: /(\=[^\=]+\=)/, token: ["comment"] },
{ regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url" }, // links
{ regex: /\[\[[^\[\]]+\]\]/, token: "org-image" }, // image
{ regex: /\[[xX\s\-\_]\]/, token: "qualifier org-toggle" }, // checkbox
{ regex: /\#\+(?:(BEGIN|begin))_[a-zA-Z]*/, token: "comment", next: "env", sol: true }, // comments
{ regex: /:?[A-Z_]+\:.*/, token: "comment", sol: true }, // property drawers
{ regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", "qualifier"], sol: true }, // environments
{ regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"] },
],
env: [
{ regex: /\#\+(?:(END|end))_[a-zA-Z]*/, token: "comment", next: "start", sol: true },
{ regex: /.*/, token: "comment" },
],
});
CodeMirror.registerHelper("fold", "orgmode", function(cm, start) {
// init
const levelToMatch = headerLevel(start.line);
// no folding needed
if (levelToMatch === null) return;
// find folding limits
const lastLine = cm.lastLine();
let end = start.line;
while (end < lastLine) {
end += 1;
const level = headerLevel(end);
if (level && level <= levelToMatch) {
end = end - 1;
break;
};
}
return {
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
to: CodeMirror.Pos(end, cm.getLine(end).length),
};
function headerLevel(lineNo) {
const line = cm.getLine(lineNo);
const match = /^\*+/.exec(line);
if (match && match.length === 1 && /header/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)))) {
return match[0].length;
}
return null;
}
});
CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
return mode.name === "orgmode" ? true : false;
}, function(cm, start) {
const drawer = isBeginningOfADrawer(start.line);
if (drawer === false) return;
// find folding limits
const lastLine = cm.lastLine();
let end = start.line;
while (end < lastLine) {
end += 1;
if (isEndOfADrawer(end)) {
break;
}
}
return {
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
to: CodeMirror.Pos(end, cm.getLine(end).length),
};
function isBeginningOfADrawer(lineNo) {
const line = cm.getLine(lineNo);
const match = /^\:.*\:$/.exec(line);
if (match && match.length === 1 && match[0] !== ":END:") {
return true;
}
return false;
}
function isEndOfADrawer(lineNo) {
const line = cm.getLine(lineNo);
return line.trim() === ":END:" ? true : false;
}
});
CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
editor.setOption("extraKeys", {
"Tab": (cm) => org_cycle(cm),
"Shift-Tab": (cm) => fn("shifttab", org_shifttab(cm)),
"Alt-Left": (cm) => org_metaleft(cm),
"Alt-Right": (cm) => org_metaright(cm),
"Alt-Enter": (cm) => org_meta_return(cm),
"Alt-Up": (cm) => org_metaup(cm),
"Alt-Down": (cm) => org_metadown(cm),
"Shift-Alt-Left": (cm) => org_shiftmetaleft(cm),
"Shift-Alt-Right": (cm) => org_shiftmetaright(cm),
"Shift-Alt-Enter": (cm) => org_insert_todo_heading(cm),
"Shift-Left": (cm) => org_shiftleft(cm),
"Shift-Right": (cm) => org_shiftright(cm),
});
fn("shifttab", org_set_fold(editor));
editor.on("mousedown", toggleHandler);
editor.on("touchstart", toggleHandler);
editor.on("gutterClick", foldLine);
// fold everything except headers by default
editor.operation(function() {
for (let i = 0; i < editor.lineCount(); i++) {
if (/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false) {
fold(editor, CodeMirror.Pos(i, 0));
}
}
});
return CodeMirror.orgmode.destroy.bind(this, editor);
});
CodeMirror.registerHelper("orgmode", "destroy", (editor) => {
editor.off("mousedown", toggleHandler);
editor.off("touchstart", toggleHandler);
editor.off("gutterClick", foldLine);
});
function foldLine(cm, line) {
const cursor = { line: line, ch: 0 };
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
}
let widgets = [];
function toggleHandler(cm, e) {
const position = cm.coordsChar({
left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX),
top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY),
}, "page");
const token = cm.getTokenAt(position);
_disableSelection();
if (/org-level-star/.test(token.type)) {
_preventIfShould();
_foldHeadline();
_disableSelection();
} else if (/org-toggle/.test(token.type)) {
_preventIfShould();
_toggleCheckbox();
_disableSelection();
} else if (/org-todo/.test(token.type)) {
_preventIfShould();
_toggleTodo();
_disableSelection();
} else if (/org-done/.test(token.type)) {
_preventIfShould();
_toggleDone();
_disableSelection();
} else if (/org-priority/.test(token.type)) {
_preventIfShould();
_togglePriority();
_disableSelection();
} else if (/org-url/.test(token.type)) {
_disableSelection();
_navigateLink();
} else if (/org-image/.test(token.type)) {
_disableSelection();
_toggleImageWidget();
}
function _preventIfShould() {
if ("ontouchstart" in window) e.preventDefault();
}
function _disableSelection() {
cm.on("beforeSelectionChange", _onSelectionChangeHandler);
function _onSelectionChangeHandler(cm, obj) {
obj.update([{
anchor: position,
head: position,
}]);
cm.off("beforeSelectionChange", _onSelectionChangeHandler);
}
}
function _foldHeadline() {
const line = position.line;
if (line >= 0) {
const cursor = { line: line, ch: 0 };
isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
}
}
function _toggleCheckbox() {
const line = position.line;
const content = cm.getRange(
{ line: line, ch: token.start },
{ line: line, ch: token.end },
);
const new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
cm.replaceRange(
new_content,
{ line: line, ch: token.start },
{ line: line, ch: token.end },
);
}
function _toggleTodo() {
const line = position.line;
cm.replaceRange(
"DONE",
{ line: line, ch: token.start },
{ line: line, ch: token.end },
);
}
function _toggleDone() {
const line = position.line;
cm.replaceRange(
"TODO",
{ line: line, ch: token.start },
{ line: line, ch: token.end },
);
}
function _togglePriority() {
const PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
const line = position.line;
const content = cm.getRange({ line: line, ch: token.start }, { line: line, ch: token.end });
const new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
cm.replaceRange(
new_content,
{ line: line, ch: token.start },
{ line: line, ch: token.end },
);
}
function _toggleImageWidget() {
const exist = !!widgets
.filter((line) => line === position.line)[0];
if (exist === false) {
if (!token.string.match(/\[\[(.*)\]\]/)) return null;
const $node = _buildImage(RegExp.$1);
const widget = cm.addLineWidget(position.line, $node, { coverGutter: false });
widgets.push(position.line);
$node.addEventListener("click", closeWidget);
function closeWidget() {
widget.clear();
$node.removeEventListener("click", closeWidget);
widgets = widgets.filter((line) => line !== position.line);
}
}
function _buildImage(src) {
const $el = document.createElement("div");
const $img = document.createElement("img");
if (/^https?\:\/\//.test(src)) {
$img.src = src;
} else {
const root_path = dirname(window.location.pathname.replace(/^\/view/, ""));
const img_path = src;
$img.src = "/api/files/cat?path="+encodeURIComponent(pathBuilder(root_path, img_path));
}
$el.appendChild($img);
return $el;
}
return null;
}
function _navigateLink() {
token.string.match(/\[\[(.*?)\]\[/);
const link = RegExp.$1;
if (!link) return;
let open = "_blank";
const isMobile = screen.availWidth < screen.availHeight;
if (!document.querySelector(".component_fab img.component_icon[alt=\"save\"]")) {
open = "_self";
} else if (isMobile) {
open = "_self";
}
if (/^https?\:\/\//.test(link)) {
window.open(link, open);
} else {
const root_path = dirname(window.location.pathname.replace(/^\/view/, ""));
const share = currentShare();
const url = share ? "/view"+pathBuilder(root_path, link)+"?share="+share : "/view"+pathBuilder(root_path, link);
window.open(url, open);
}
}
}
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/perl/perl.js";
CodeMirror.__mode = "perl";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/php/php.js";
CodeMirror.__mode = "php";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/properties/properties.js";
CodeMirror.__mode = "properties";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/python/python.js";
CodeMirror.__mode = "python";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/r/r.js";
CodeMirror.__mode = "r";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/ruby/ruby.js";
CodeMirror.__mode = "ruby";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/rust/rust.js";
CodeMirror.__mode = "rust";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/sass/sass.js";
CodeMirror.__mode = "sass";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/shell/shell.js";
CodeMirror.__mode = "shell";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/sparql/sparql.js";
CodeMirror.__mode = "sparql";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/spreadsheet/spreadsheet.js";
CodeMirror.__mode = "spreadsheet";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/sql/sql.js";
CodeMirror.__mode = "sql";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/stex/stex.js";
CodeMirror.__mode = "stex";
export default CodeMirror;
@@ -0,0 +1 @@
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/xml/xml.js";
CodeMirror.__mode = "xml";
export default CodeMirror;
@@ -0,0 +1,4 @@
import "../../../lib/vendor/codemirror/mode/gfm/gfm.js";
import "../../../lib/vendor/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js";
CodeMirror.__mode = "yaml-frontmatter";
export default CodeMirror;
@@ -0,0 +1,3 @@
import "../../../lib/vendor/codemirror/mode/yaml/yaml.js";
CodeMirror.__mode = "yaml";
export default CodeMirror;
@@ -0,0 +1,8 @@
export default function(editor) {
CodeMirror.orgmode.init(editor, (key, value) => {
if (key === "shifttab") {
// org_shifttab(this.state.editor)
// this.props.onFoldChange(value);
}
});
}
@@ -167,6 +167,9 @@ export default function(render, { mime }) {
hls.attachMedia($video);
}),
rxjs.mergeMap(() => rxjs.fromEvent($video, "loadeddata")),
// rxjs.tap(() => renderMenubar(buildMenubar(
// menubarDownload(),
// ))),
rxjs.mergeMap(() => {
const $loader = qs($page, ".loader");
$loader.replaceChildren(createElement(`<img style="height:170px;cursor:pointer;filter:brightness(0.5) invert(1);" src="${ICON.PLAY}" />`));
@@ -177,9 +180,6 @@ export default function(render, { mime }) {
setSeek(0);
return rxjs.fromEvent($loader, "click").pipe(rxjs.mapTo($loader));
}),
// rxjs.tap(() => renderMenubar(buildMenubar(
// menubarDownload(),
// ))),
rxjs.tap(($loader) => {
$loader.classList.add("hidden")
const $control = qs($page, ".videoplayer_control");
+1 -1
View File
@@ -16,7 +16,7 @@ export function getFile$() {
export function saveFile$() {
return rxjs.pipe(
rxjs.delay(2000),
rxjs.tap((content) => console.log("SAVED", content)),
rxjs.tap((content) => console.log("SAVED")),
);
}
@@ -0,0 +1,9 @@
import rxjs from "../../lib/rx.js";
import ajax from "../../lib/ajax.js";
export function fileOptions(path) {
return ajax({
url: `/api/files/cat?path=${path}`,
method: "OPTIONS",
}).pipe(rxjs.map((res) => res.responseHeaders.allow.replace(/\r/, "").split(", ")));
}