🎉 web editor
This commit is contained in:
parent
219bb6356f
commit
2e2b801ce3
70
dev/debugger.js
Normal file
70
dev/debugger.js
Normal file
@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
// CLEAR DEBUGGING FLAGS //
|
||||
// line occupancy in built js source
|
||||
let debug_source_line_occupancy = {};
|
||||
|
||||
|
||||
// EMPTY ERROR BUFFER FOR TIME UNTIL HTML IS AVAILABLE //
|
||||
let javascript_error_buffer = [];
|
||||
|
||||
|
||||
function debug_javascript_error_message(error_event){
|
||||
// TRANSLATE TO SOURCE FILE'S LINES //
|
||||
// find source file occupying this line
|
||||
let source_file = null;
|
||||
let source_line = 0;
|
||||
for(var one_source_file in debug_source_line_occupancy){
|
||||
let occupancy = debug_source_line_occupancy[one_source_file];
|
||||
if(error_event.lineno >= occupancy.start && (occupancy.end === undefined || error_event.lineno <= occupancy.end)){
|
||||
// remember source file name
|
||||
source_file = one_source_file;
|
||||
|
||||
// translate compiled line to source file line
|
||||
source_line = error_event.lineno - debug_source_line_occupancy[source_file].start + 1;
|
||||
|
||||
// stop searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// OUTPUT MESSAGE //
|
||||
// build line info
|
||||
let line_info_string;
|
||||
if(source_file !== null){
|
||||
line_info_string = source_file + " line " + source_line;
|
||||
} else {
|
||||
line_info_string = "raw source" + " line " + error_event.lineno;
|
||||
}
|
||||
|
||||
// send to callback
|
||||
my_error_callback("(" + line_info_string + ") " + error_event.message, "javascript");
|
||||
}
|
||||
|
||||
|
||||
// EMPTY ERROR BUFFER WHEN HTML IS AVAILABLE //
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
// print messages
|
||||
for(var one_error_event of javascript_error_buffer){
|
||||
debug_javascript_error_message(one_error_event);
|
||||
}
|
||||
|
||||
// disable buffer
|
||||
javascript_error_buffer = null;
|
||||
});
|
||||
|
||||
window.addEventListener("error", function(error_event){
|
||||
// OUTPUT //
|
||||
if(javascript_error_buffer !== null){
|
||||
// add to buffer
|
||||
javascript_error_buffer.push(error_event);
|
||||
|
||||
} else {
|
||||
// output directly
|
||||
debug_javascript_error_message(error_event);
|
||||
}
|
||||
|
||||
|
||||
// DON'T CANCEL EVENT //
|
||||
return false;
|
||||
});
|
152
dev/index.css
Normal file
152
dev/index.css
Normal file
@ -0,0 +1,152 @@
|
||||
:root {
|
||||
--onedark-bg: #21252b;
|
||||
--onedark-bg-light: #2c313a;
|
||||
--onedark-white: #c5cad3;
|
||||
--onedark-gray: #828997;
|
||||
--onedark-gray-dark: #5c6370;
|
||||
--onedark-gray-dark-dark: #454b54;
|
||||
--onedark-red: #e06c75;
|
||||
--onedark-orange: #d19a66;
|
||||
--onedark-yellow: #e5c07b;
|
||||
--onedark-green: #98c379;
|
||||
--onedark-cyan: #56b6c2;
|
||||
--onedark-blue: #61afef;
|
||||
--onedark-purple: #c678dd;
|
||||
}
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
background-color: var(--onedark-bg);
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
|
||||
span.line {
|
||||
display: block;
|
||||
color: var(--onedark-white);
|
||||
}
|
||||
span.line.stderr-debug { color: var(--onedark-green); }
|
||||
span.line.stderr-info { color: var(--onedark-blue); }
|
||||
span.line.stderr-warning { color: var(--onedark-orange); }
|
||||
span.line.stderr-error { color: var(--onedark-red); }
|
||||
span.line.stderr-javascript { color: var(--onedark-purple); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
div.collection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
div.collection div.item {
|
||||
flex-basis: 50%;
|
||||
flex: 20rem;
|
||||
|
||||
height: calc(100vh - 4em);
|
||||
max-width: 100%;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
margin: 0rem;
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
div.collection div.item {
|
||||
flex-basis: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.button {
|
||||
align-items: center;
|
||||
|
||||
padding: 1rem 1.5rem;
|
||||
width: fit-content;
|
||||
|
||||
color: var(--onedark-white);
|
||||
|
||||
background-color: var(--onedark-bg-light);
|
||||
}
|
||||
|
||||
.button:not(.one-line) {
|
||||
display: inline-flex;
|
||||
margin: 1rem 1rem 0rem 0rem;
|
||||
}
|
||||
.button.one-line {
|
||||
display: flex;
|
||||
margin: 0rem 0rem 1rem 0rem;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--onedark-gray-dark-dark);
|
||||
}
|
||||
|
||||
.button .text {
|
||||
display: inline-flex;
|
||||
flex-flow: column;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.button img.icon {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.button .extra {
|
||||
color: var(--onedark-gray);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
div.collection div.item.editor > div.prism-live {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
div.collection div.item.console div.controls {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
#console {
|
||||
margin: 0px;
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 1rem;
|
||||
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
background-color: var(--onedark-bg-light);
|
||||
}
|
55
dev/index.js
Normal file
55
dev/index.js
Normal file
@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
let my_output_callback = function(text){
|
||||
let line_list = text.split("\n");
|
||||
|
||||
for(let one_line of line_list){
|
||||
let span_one_line = document.createElement("span");
|
||||
|
||||
span_one_line.classList.add("line");
|
||||
span_one_line.textContent = one_line;
|
||||
|
||||
document.getElementById("console").appendChild(span_one_line);
|
||||
document.getElementById("console").scrollTop = document.getElementById("console").scrollHeight;
|
||||
}
|
||||
}
|
||||
let my_error_callback = function(text, type){
|
||||
let span_one_line = document.createElement("span");
|
||||
|
||||
span_one_line.classList.add("line", "stderr-" + type);
|
||||
span_one_line.textContent = text + "\n";
|
||||
|
||||
document.getElementById("console").appendChild(span_one_line);
|
||||
document.getElementById("console").scrollTop = document.getElementById("console").scrollHeight;
|
||||
}
|
||||
|
||||
let juicescript = new Juicescript({
|
||||
callback: {
|
||||
stdout: my_output_callback,
|
||||
stderr: my_error_callback
|
||||
}
|
||||
});
|
||||
|
||||
function button_run(){
|
||||
// clear console
|
||||
document.getElementById("console").innerHTML = "";
|
||||
|
||||
// get source code from editor
|
||||
let juice_program = document.getElementById("editor").value;
|
||||
|
||||
// parse source code
|
||||
/**/const parse_start = performance.now();
|
||||
let parse_success = juicescript.parse(juice_program);
|
||||
/**/const parse_end = performance.now();
|
||||
/**/juicescript.io.stderr.info("Parsing took " + (parse_end - parse_start) + "ms");
|
||||
|
||||
// execute program
|
||||
if(parse_success){
|
||||
/**/const run_start = performance.now();
|
||||
juicescript.run();
|
||||
/**/const run_end = performance.now();
|
||||
/**/juicescript.io.stderr.info("Running took " + (run_end - run_start) + "ms");
|
||||
|
||||
} else {
|
||||
juicescript.io.stderr.info("Not executing program due to parse error");
|
||||
}
|
||||
}
|
214
dev/index.php
214
dev/index.php
@ -5,126 +5,18 @@
|
||||
<link rel="shortcut icon" href="/icon.png">
|
||||
<title>Juicescript dev env</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--onedark-bg: #21252b;
|
||||
--onedark-bg-light: #2c313a;
|
||||
--onedark-white: #c5cad3;
|
||||
--onedark-gray: #828997;
|
||||
--onedark-gray-dark: #5c6370;
|
||||
--onedark-red: #e06c75;
|
||||
--onedark-orange: #d19a66;
|
||||
--onedark-yellow: #e5c07b;
|
||||
--onedark-green: #98c379;
|
||||
--onedark-cyan: #56b6c2;
|
||||
--onedark-blue: #61afef;
|
||||
--onedark-purple: #c678dd;
|
||||
}
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
body {
|
||||
background-color: var(--onedark-bg);
|
||||
}
|
||||
div.text-output-container {
|
||||
height: calc(100vh - 4rem - 4px);
|
||||
width: calc(100vw - 4rem - 4px);
|
||||
padding: 2rem;
|
||||
}
|
||||
#text-output-area {
|
||||
height: calc(100% - 2rem);
|
||||
width: calc(100% - 2rem);
|
||||
padding: 1rem;
|
||||
margin: 0px;
|
||||
|
||||
overflow: auto;
|
||||
font-size: 1rem;
|
||||
|
||||
background-color: var(--onedark-bg-light);
|
||||
border: none;
|
||||
}
|
||||
span.line {
|
||||
display: block;
|
||||
color: var(--onedark-white);
|
||||
}
|
||||
span.line.stderr-debug { color: var(--onedark-green); }
|
||||
span.line.stderr-info { color: var(--onedark-blue); }
|
||||
span.line.stderr-warning { color: var(--onedark-orange); }
|
||||
span.line.stderr-error { color: var(--onedark-red); }
|
||||
span.line.stderr-javascript { color: var(--onedark-purple); }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/prism/prism-onedark.css">
|
||||
<link rel="stylesheet" href="/prism/prism-line-numbers.css">
|
||||
<link rel="stylesheet" href="/prism/prism-live.css">
|
||||
<link rel="stylesheet" href="/tabler-icons/tabler-icons.min.css">
|
||||
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
|
||||
<script type="text/javascript">
|
||||
// CLEAR DEBUGGING FLAGS //
|
||||
// line occupancy in built js source
|
||||
let debug_source_line_occupancy = {};
|
||||
|
||||
|
||||
// EMPTY ERROR BUFFER FOR TIME UNTIL HTML IS AVAILABLE //
|
||||
let javascript_error_buffer = [];
|
||||
|
||||
|
||||
function debug_javascript_error_message(error_event){
|
||||
// TRANSLATE TO SOURCE FILE'S LINES //
|
||||
// find source file occupying this line
|
||||
let source_file = null;
|
||||
let source_line = 0;
|
||||
for(var one_source_file in debug_source_line_occupancy){
|
||||
let occupancy = debug_source_line_occupancy[one_source_file];
|
||||
if(error_event.lineno >= occupancy.start && (occupancy.end === undefined || error_event.lineno <= occupancy.end)){
|
||||
// remember source file name
|
||||
source_file = one_source_file;
|
||||
|
||||
// translate compiled line to source file line
|
||||
source_line = error_event.lineno - debug_source_line_occupancy[source_file].start + 1;
|
||||
|
||||
// stop searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// OUTPUT MESSAGE //
|
||||
// build line info
|
||||
let line_info_string;
|
||||
if(source_file !== null){
|
||||
line_info_string = source_file + " line " + source_line;
|
||||
} else {
|
||||
line_info_string = "raw source" + " line " + error_event.lineno;
|
||||
}
|
||||
|
||||
// send to callback
|
||||
my_error_callback("(" + line_info_string + ") " + error_event.message, "javascript");
|
||||
}
|
||||
|
||||
|
||||
// EMPTY ERROR BUFFER WHEN HTML IS AVAILABLE //
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
// print messages
|
||||
for(var one_error_event of javascript_error_buffer){
|
||||
debug_javascript_error_message(one_error_event);
|
||||
}
|
||||
|
||||
// disable buffer
|
||||
javascript_error_buffer = null;
|
||||
});
|
||||
|
||||
window.addEventListener("error", function(error_event){
|
||||
// OUTPUT //
|
||||
if(javascript_error_buffer !== null){
|
||||
// add to buffer
|
||||
javascript_error_buffer.push(error_event);
|
||||
|
||||
} else {
|
||||
// output directly
|
||||
debug_javascript_error_message(error_event);
|
||||
}
|
||||
|
||||
|
||||
// DON'T CANCEL EVENT //
|
||||
return false;
|
||||
});
|
||||
<?php
|
||||
require("debugger.js");
|
||||
echo("\n");
|
||||
?>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@ -160,69 +52,35 @@
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
let juice_program = <?php echo(json_encode(file_get_contents("juice-program.jce"))); ?>;
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
let my_output_callback = function(text){
|
||||
let line_list = text.split("\n");
|
||||
|
||||
for(let one_line of line_list){
|
||||
let span_one_line = document.createElement("span");
|
||||
|
||||
span_one_line.classList.add("line");
|
||||
span_one_line.textContent = one_line;
|
||||
|
||||
document.getElementById("text-output-area").appendChild(span_one_line);
|
||||
document.getElementById("text-output-area").scrollTop = document.getElementById("text-output-area").scrollHeight;
|
||||
}
|
||||
}
|
||||
let my_error_callback = function(text, type){
|
||||
let span_one_line = document.createElement("span");
|
||||
|
||||
span_one_line.classList.add("line", "stderr-" + type);
|
||||
span_one_line.textContent = text + "\n";
|
||||
|
||||
document.getElementById("text-output-area").appendChild(span_one_line);
|
||||
document.getElementById("text-output-area").scrollTop = document.getElementById("text-output-area").scrollHeight;
|
||||
}
|
||||
|
||||
let juicescript = new Juicescript({
|
||||
callback: {
|
||||
stdout: my_output_callback,
|
||||
stderr: my_error_callback
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
// handle missing source file
|
||||
if(juice_program === false){
|
||||
juicescript.io.stderr.error("No source file found! Add one at 'dev/juice-program.jce'");
|
||||
return;
|
||||
}
|
||||
|
||||
// parse source code
|
||||
/**/const parse_start = performance.now();
|
||||
let parse_success = juicescript.parse(juice_program);
|
||||
/**/const parse_end = performance.now();
|
||||
/**/juicescript.io.stderr.info("Parsing took " + (parse_end - parse_start) + "ms");
|
||||
|
||||
// execute program
|
||||
if(parse_success){
|
||||
/**/const run_start = performance.now();
|
||||
juicescript.run();
|
||||
/**/const run_end = performance.now();
|
||||
/**/juicescript.io.stderr.info("Running took " + (run_end - run_start) + "ms");
|
||||
|
||||
} else {
|
||||
juicescript.io.stderr.info("Not executing program due to parse error");
|
||||
}
|
||||
});
|
||||
<?php
|
||||
echo("debug_source_line_occupancy[\"index.js\"] = {start: new Error().lineNumber};");
|
||||
require("index.js");
|
||||
echo("\n");
|
||||
echo("debug_source_line_occupancy[\"index.js\"].end = new Error().lineNumber;");
|
||||
?>
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="text-output-container">
|
||||
<pre id="text-output-area"></pre>
|
||||
<div class="collection">
|
||||
<div class="item editor">
|
||||
<textarea id="editor" class="prism-live line-numbers language-ruby fill"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="item console">
|
||||
<div class="button-list controls">
|
||||
<a onclick="button_run()" class="button one-line">
|
||||
<span class="icon ti ti-player-play"></span>
|
||||
<span class="text">Run</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<pre id="console"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/prism/bliss.shy.min.js"></script>
|
||||
<script src="/prism/prism-juicescript.js"></script>
|
||||
<script src="/prism/prism-line-numbers.js"></script>
|
||||
<script src="/prism/prism-live.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
2
dev/prism/bliss.shy.min.js
vendored
Normal file
2
dev/prism/bliss.shy.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1488
dev/prism/prism-juicescript.js
Normal file
1488
dev/prism/prism-juicescript.js
Normal file
File diff suppressed because it is too large
Load Diff
40
dev/prism/prism-line-numbers.css
Normal file
40
dev/prism/prism-line-numbers.css
Normal file
@ -0,0 +1,40 @@
|
||||
pre[class*="language-"].line-numbers {
|
||||
position: relative;
|
||||
padding-left: 3.8em;
|
||||
counter-reset: linenumber;
|
||||
}
|
||||
|
||||
pre[class*="language-"].line-numbers > code {
|
||||
position: relative;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
.line-numbers .line-numbers-rows {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
font-size: 100%;
|
||||
left: -3.8em;
|
||||
width: 3em; /* works for line-numbers below 1000 lines */
|
||||
letter-spacing: -1px;
|
||||
border-right: 1px solid #999;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
.line-numbers-rows > span {
|
||||
display: block;
|
||||
counter-increment: linenumber;
|
||||
}
|
||||
|
||||
.line-numbers-rows > span:before {
|
||||
content: counter(linenumber);
|
||||
color: #999;
|
||||
display: block;
|
||||
padding-right: 0.8em;
|
||||
text-align: right;
|
||||
}
|
252
dev/prism/prism-line-numbers.js
Normal file
252
dev/prism/prism-line-numbers.js
Normal file
@ -0,0 +1,252 @@
|
||||
(function () {
|
||||
|
||||
if (typeof Prism === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin name which is used as a class name for <pre> which is activating the plugin
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
var PLUGIN_NAME = 'line-numbers';
|
||||
|
||||
/**
|
||||
* Regular expression used for determining line breaks
|
||||
*
|
||||
* @type {RegExp}
|
||||
*/
|
||||
var NEW_LINE_EXP = /\n(?!$)/g;
|
||||
|
||||
|
||||
/**
|
||||
* Global exports
|
||||
*/
|
||||
var config = Prism.plugins.lineNumbers = {
|
||||
/**
|
||||
* Get node for provided line number
|
||||
*
|
||||
* @param {Element} element pre element
|
||||
* @param {number} number line number
|
||||
* @returns {Element|undefined}
|
||||
*/
|
||||
getLine: function (element, number) {
|
||||
if (element.tagName !== 'PRE' || !element.classList.contains(PLUGIN_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lineNumberRows = element.querySelector('.line-numbers-rows');
|
||||
if (!lineNumberRows) {
|
||||
return;
|
||||
}
|
||||
var lineNumberStart = parseInt(element.getAttribute('data-start'), 10) || 1;
|
||||
var lineNumberEnd = lineNumberStart + (lineNumberRows.children.length - 1);
|
||||
|
||||
if (number < lineNumberStart) {
|
||||
number = lineNumberStart;
|
||||
}
|
||||
if (number > lineNumberEnd) {
|
||||
number = lineNumberEnd;
|
||||
}
|
||||
|
||||
var lineIndex = number - lineNumberStart;
|
||||
|
||||
return lineNumberRows.children[lineIndex];
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the line numbers of the given element.
|
||||
*
|
||||
* This function will not add line numbers. It will only resize existing ones.
|
||||
*
|
||||
* @param {HTMLElement} element A `<pre>` element with line numbers.
|
||||
* @returns {void}
|
||||
*/
|
||||
resize: function (element) {
|
||||
resizeElements([element]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the plugin can assume that the units font sizes and margins are not depended on the size of
|
||||
* the current viewport.
|
||||
*
|
||||
* Setting this to `true` will allow the plugin to do certain optimizations for better performance.
|
||||
*
|
||||
* Set this to `false` if you use any of the following CSS units: `vh`, `vw`, `vmin`, `vmax`.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
assumeViewportIndependence: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes the given elements.
|
||||
*
|
||||
* @param {HTMLElement[]} elements
|
||||
*/
|
||||
function resizeElements(elements) {
|
||||
elements = elements.filter(function (e) {
|
||||
var codeStyles = getStyles(e);
|
||||
var whiteSpace = codeStyles['white-space'];
|
||||
return whiteSpace === 'pre-wrap' || whiteSpace === 'pre-line';
|
||||
});
|
||||
|
||||
if (elements.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var infos = elements.map(function (element) {
|
||||
var codeElement = element.querySelector('code');
|
||||
var lineNumbersWrapper = element.querySelector('.line-numbers-rows');
|
||||
if (!codeElement || !lineNumbersWrapper) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
var lineNumberSizer = element.querySelector('.line-numbers-sizer');
|
||||
var codeLines = codeElement.textContent.split(NEW_LINE_EXP);
|
||||
|
||||
if (!lineNumberSizer) {
|
||||
lineNumberSizer = document.createElement('span');
|
||||
lineNumberSizer.className = 'line-numbers-sizer';
|
||||
|
||||
codeElement.appendChild(lineNumberSizer);
|
||||
}
|
||||
|
||||
lineNumberSizer.innerHTML = '0';
|
||||
lineNumberSizer.style.display = 'block';
|
||||
|
||||
var oneLinerHeight = lineNumberSizer.getBoundingClientRect().height;
|
||||
lineNumberSizer.innerHTML = '';
|
||||
|
||||
return {
|
||||
element: element,
|
||||
lines: codeLines,
|
||||
lineHeights: [],
|
||||
oneLinerHeight: oneLinerHeight,
|
||||
sizer: lineNumberSizer,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
infos.forEach(function (info) {
|
||||
var lineNumberSizer = info.sizer;
|
||||
var lines = info.lines;
|
||||
var lineHeights = info.lineHeights;
|
||||
var oneLinerHeight = info.oneLinerHeight;
|
||||
|
||||
lineHeights[lines.length - 1] = undefined;
|
||||
lines.forEach(function (line, index) {
|
||||
if (line && line.length > 1) {
|
||||
var e = lineNumberSizer.appendChild(document.createElement('span'));
|
||||
e.style.display = 'block';
|
||||
e.textContent = line;
|
||||
} else {
|
||||
lineHeights[index] = oneLinerHeight;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
infos.forEach(function (info) {
|
||||
var lineNumberSizer = info.sizer;
|
||||
var lineHeights = info.lineHeights;
|
||||
|
||||
var childIndex = 0;
|
||||
for (var i = 0; i < lineHeights.length; i++) {
|
||||
if (lineHeights[i] === undefined) {
|
||||
lineHeights[i] = lineNumberSizer.children[childIndex++].getBoundingClientRect().height;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infos.forEach(function (info) {
|
||||
var lineNumberSizer = info.sizer;
|
||||
var wrapper = info.element.querySelector('.line-numbers-rows');
|
||||
|
||||
lineNumberSizer.style.display = 'none';
|
||||
lineNumberSizer.innerHTML = '';
|
||||
|
||||
info.lineHeights.forEach(function (height, lineNumber) {
|
||||
wrapper.children[lineNumber].style.height = height + 'px';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns style declarations for the element
|
||||
*
|
||||
* @param {Element} element
|
||||
*/
|
||||
function getStyles(element) {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.getComputedStyle ? getComputedStyle(element) : (element.currentStyle || null);
|
||||
}
|
||||
|
||||
var lastWidth = undefined;
|
||||
window.addEventListener('resize', function () {
|
||||
if (config.assumeViewportIndependence && lastWidth === window.innerWidth) {
|
||||
return;
|
||||
}
|
||||
lastWidth = window.innerWidth;
|
||||
|
||||
resizeElements(Array.prototype.slice.call(document.querySelectorAll('pre.' + PLUGIN_NAME)));
|
||||
});
|
||||
|
||||
Prism.hooks.add('complete', function (env) {
|
||||
if (!env.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
var code = /** @type {Element} */ (env.element);
|
||||
var pre = /** @type {HTMLElement} */ (code.parentNode);
|
||||
|
||||
// works only for <code> wrapped inside <pre> (not inline)
|
||||
if (!pre || !/pre/i.test(pre.nodeName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort if line numbers already exists
|
||||
if (code.querySelector('.line-numbers-rows')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only add line numbers if <code> or one of its ancestors has the `line-numbers` class
|
||||
if (!Prism.util.isActive(code, PLUGIN_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the class 'line-numbers' from the <code>
|
||||
code.classList.remove(PLUGIN_NAME);
|
||||
// Add the class 'line-numbers' to the <pre>
|
||||
pre.classList.add(PLUGIN_NAME);
|
||||
|
||||
var match = env.code.match(NEW_LINE_EXP);
|
||||
var linesNum = match ? match.length + 1 : 1;
|
||||
var lineNumbersWrapper;
|
||||
|
||||
var lines = new Array(linesNum + 1).join('<span></span>');
|
||||
|
||||
lineNumbersWrapper = document.createElement('span');
|
||||
lineNumbersWrapper.setAttribute('aria-hidden', 'true');
|
||||
lineNumbersWrapper.className = 'line-numbers-rows';
|
||||
lineNumbersWrapper.innerHTML = lines;
|
||||
|
||||
if (pre.hasAttribute('data-start')) {
|
||||
pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1);
|
||||
}
|
||||
|
||||
env.element.appendChild(lineNumbersWrapper);
|
||||
|
||||
resizeElements([pre]);
|
||||
|
||||
Prism.hooks.run('line-numbers', env);
|
||||
});
|
||||
|
||||
Prism.hooks.add('line-numbers', function (env) {
|
||||
env.plugins = env.plugins || {};
|
||||
env.plugins.lineNumbers = true;
|
||||
});
|
||||
|
||||
}());
|
66
dev/prism/prism-live.css
Normal file
66
dev/prism/prism-live.css
Normal file
@ -0,0 +1,66 @@
|
||||
div.prism-live {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
textarea.prism-live,
|
||||
pre.prism-live {
|
||||
padding: .2rem .5rem;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
textarea.prism-live {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
color: transparent;
|
||||
/* color: hsla(0,0%,50%,.4); */
|
||||
cursor: text;
|
||||
white-space: pre;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
--selection-background: hsl(320, 80%, 25%);
|
||||
--selection-color: hsla(0, 0%, 100%, .8);
|
||||
}
|
||||
|
||||
@supports (not (caret-color: black)) and (-webkit-text-fill-color: black) {
|
||||
textarea.prism-live {
|
||||
color: inherit;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Setting specific colors is needed
|
||||
* because otherwise Firefox shows blank text */
|
||||
textarea.prism-live::-moz-selection {
|
||||
background: var(--selection-background);
|
||||
color: var(--selection-color);
|
||||
}
|
||||
|
||||
textarea.prism-live::selection {
|
||||
background: var(--selection-background);
|
||||
color: var(--selection-color);
|
||||
}
|
||||
|
||||
pre.prism-live {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
--scrollbar-width: 17px;
|
||||
padding-bottom: var(--scrollbar-width);
|
||||
padding-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
pre.prism-live > code:empty::before {
|
||||
content: " "
|
||||
}
|
845
dev/prism/prism-live.js
Normal file
845
dev/prism/prism-live.js
Normal file
@ -0,0 +1,845 @@
|
||||
/**
|
||||
Prism Live: Code editor based on Prism.js
|
||||
Works best in Chrome. Currently only very basic support in other browsers (no snippets, no shortcuts)
|
||||
@author Lea Verou
|
||||
*/
|
||||
(async function() {
|
||||
|
||||
const CURRENT_URL = document.currentScript? new URL(document.currentScript.src) : null;
|
||||
|
||||
if (!window.Bliss) {
|
||||
// Load Bliss if not loaded
|
||||
console.log("Bliss not loaded. Loading remotely from blissfuljs.com");
|
||||
|
||||
let bliss = document.createElement("script");
|
||||
bliss.src = "https://blissfuljs.com/bliss.shy.min.js";
|
||||
document.head.appendChild(bliss);
|
||||
|
||||
await new Promise(resolve => bliss.onload = resolve);
|
||||
}
|
||||
|
||||
var $ = Bliss, $$ = Bliss.$;
|
||||
var ready = Promise.resolve();
|
||||
|
||||
if (CURRENT_URL) {
|
||||
// Tiny dynamic loader. Use e.g. ?load=css,markup,javascript to load components
|
||||
var load = CURRENT_URL.searchParams.get("load");
|
||||
|
||||
if (load !== null) {
|
||||
var files = ["../prism-live.css"];
|
||||
|
||||
if (load) {
|
||||
files.push(...load.split(/,/).map(c => /\./.test(c)? c : `prism-live-${c}.js`));
|
||||
}
|
||||
|
||||
ready = Promise.all(files.map(url => $.load(url, CURRENT_URL)));
|
||||
}
|
||||
}
|
||||
|
||||
var superKey = navigator.platform.indexOf("Mac") === 0? "metaKey" : "ctrlKey";
|
||||
|
||||
var _ = Prism.Live = class PrismLive {
|
||||
constructor(source) {
|
||||
this.source = source;
|
||||
this.sourceType = source.nodeName.toLowerCase();
|
||||
|
||||
this.wrapper = $.create({
|
||||
className: "prism-live",
|
||||
around: this.source
|
||||
});
|
||||
|
||||
if (this.sourceType === "textarea") {
|
||||
this.textarea = this.source;
|
||||
this.code = $.create("code");
|
||||
|
||||
this.pre = $.create("pre", {
|
||||
className: this.textarea.className + " no-whitespace-normalization",
|
||||
contents: this.code,
|
||||
before: this.textarea
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.pre = this.source;
|
||||
// Normalize once, to fix indentation from markup and then remove normalization
|
||||
// so we can enter blank lines etc
|
||||
|
||||
// Prism.plugins.NormalizeWhitespace.normalize($("code", this.pre), {});
|
||||
this.pre.classList.add("no-whitespace-normalization");
|
||||
this.code = $("code", this.pre);
|
||||
|
||||
this.textarea = $.create("textarea", {
|
||||
className: this.pre.className,
|
||||
value: this.pre.textContent,
|
||||
after: this.pre
|
||||
});
|
||||
}
|
||||
|
||||
_.all.set(this.textarea, this);
|
||||
_.all.set(this.pre, this);
|
||||
_.all.set(this.code, this);
|
||||
|
||||
this.pre.classList.add("prism-live");
|
||||
this.textarea.classList.add("prism-live");
|
||||
this.source.classList.add("prism-live-source");
|
||||
|
||||
if (self.Incrementable) {
|
||||
// TODO data-* attribute for modifier
|
||||
// TODO load dynamically if not present
|
||||
new Incrementable(this.textarea);
|
||||
}
|
||||
|
||||
$.bind(this.textarea, {
|
||||
input: evt => this.update(),
|
||||
|
||||
keyup: evt => {
|
||||
if (evt.key == "Enter") { // Enter
|
||||
// Maintain indent on line breaks
|
||||
this.insert(this.currentIndent);
|
||||
this.syncScroll();
|
||||
}
|
||||
},
|
||||
|
||||
keydown: evt => {
|
||||
if (evt.key == "Tab" && !evt.altKey) {
|
||||
// Default is to move focus off the textarea
|
||||
// this is never desirable in an editor
|
||||
evt.preventDefault();
|
||||
|
||||
if (this.tabstops && this.tabstops.length > 0) {
|
||||
// We have tabstops to go
|
||||
this.moveCaret(this.tabstops.shift());
|
||||
}
|
||||
else if (this.hasSelection) {
|
||||
var before = this.beforeCaret("\n");
|
||||
var outdent = evt.shiftKey;
|
||||
|
||||
this.selectionStart -= before.length;
|
||||
|
||||
var selection = _.adjustIndentation(this.selection, {
|
||||
relative: true,
|
||||
indentation: outdent? -1 : 1
|
||||
});
|
||||
|
||||
this.replace(selection);
|
||||
|
||||
if (outdent) {
|
||||
var indentStart = _.regexp.gm`^${this.indent}`;
|
||||
var isBeforeIndented = indentStart.test(before);
|
||||
this.selectionStart += before.length + 1 - (outdent + isBeforeIndented);
|
||||
}
|
||||
else { // Indent
|
||||
var hasLineAbove = before.length == this.selectionStart;
|
||||
this.selectionStart += before.length + 1 + !hasLineAbove;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Nothing selected, expand snippet
|
||||
var selector = _.match(this.beforeCaret(), /\S*$/);
|
||||
var snippetExpanded = this.expandSnippet(selector);
|
||||
|
||||
if (snippetExpanded) {
|
||||
requestAnimationFrame(() => $.fire(this.textarea, "input"));
|
||||
}
|
||||
else {
|
||||
this.insert(this.indent);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_.pairs[evt.key]) {
|
||||
var other = _.pairs[evt.key];
|
||||
this.wrapSelection({
|
||||
before: evt.key,
|
||||
after: other,
|
||||
outside: true
|
||||
});
|
||||
evt.preventDefault();
|
||||
}
|
||||
else if (Object.values(_.pairs).includes(evt.key)) {
|
||||
if (this.selectionStart == this.selectionEnd && this.textarea.value[this.selectionEnd] == evt.key) {
|
||||
this.selectionStart += 1;
|
||||
this.selectionEnd += 1;
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let shortcut in _.shortcuts) {
|
||||
if (_.checkShortcut(shortcut, evt)) {
|
||||
_.shortcuts[shortcut].call(this, evt);
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
click: evt => {
|
||||
var l = this.getLine();
|
||||
var v = this.value;
|
||||
var ss = this.selectionStart;
|
||||
//console.log(ss, v[ss], l, v.slice(l.start, l.end));
|
||||
},
|
||||
|
||||
"click keyup": evt => {
|
||||
if (!evt.key || evt.key.lastIndexOf("Arrow") > -1) {
|
||||
// Caret moved
|
||||
this.tabstops = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// this.syncScroll();
|
||||
this.textarea.addEventListener("scroll", this, {passive: true});
|
||||
|
||||
$.bind(window, {
|
||||
"resize": evt => this.syncStyles()
|
||||
});
|
||||
|
||||
// Copy styles with a delay
|
||||
requestAnimationFrame(() => {
|
||||
this.syncStyles();
|
||||
|
||||
var sourceCS = getComputedStyle(this.source);
|
||||
|
||||
this.pre.style.height = this.source.style.height || sourceCS.getPropertyValue("--height");
|
||||
this.pre.style.maxHeight = this.source.style.maxHeight || sourceCS.getPropertyValue("--max-height");
|
||||
this.textarea.spellcheck = this.source.spellcheck || sourceCS.getPropertyValue("--spellcheck");
|
||||
});
|
||||
|
||||
this.update();
|
||||
this.lang = (this.code.className.match(/lang(?:uage)?-(\w+)/i) || [,])[1];
|
||||
|
||||
this.observer = new MutationObserver(r => {
|
||||
if (document.activeElement !== this.textarea) {
|
||||
this.textarea.value = this.pre.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
this.observe();
|
||||
|
||||
this.source.dispatchEvent(new CustomEvent("prism-live-init", {bubbles: true, detail: this}));
|
||||
}
|
||||
|
||||
handleEvent(evt) {
|
||||
if (evt.type === "scroll") {
|
||||
this.syncScroll();
|
||||
}
|
||||
}
|
||||
|
||||
observe () {
|
||||
return this.observer && this.observer.observe(this.pre, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
|
||||
unobserve () {
|
||||
if (this.observer) {
|
||||
this.observer.takeRecords();
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
expandSnippet(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var context = this.context;
|
||||
|
||||
if (text in context.snippets || text in _.snippets) {
|
||||
// Static Snippets
|
||||
var expansion = context.snippets[text] || _.snippets[text];
|
||||
}
|
||||
else if (context.snippets.custom) {
|
||||
var expansion = context.snippets.custom.call(this, text);
|
||||
}
|
||||
|
||||
if (expansion) {
|
||||
// Insert snippet
|
||||
var stops = [];
|
||||
var replacement = [];
|
||||
var str = expansion;
|
||||
var match;
|
||||
|
||||
while (match = _.CARET_INDICATOR.exec(str)) {
|
||||
stops.push(match.index + 1);
|
||||
replacement.push(str.slice(0, match.index + match[1].length));
|
||||
str = str.slice(match.index + match[0].length);
|
||||
_.CARET_INDICATOR.lastIndex = 0;
|
||||
}
|
||||
|
||||
replacement.push(str);
|
||||
replacement = replacement.join("");
|
||||
|
||||
if (stops.length > 0) {
|
||||
// make first stop relative to end, all others relative to previous stop
|
||||
stops[0] -= replacement.length;
|
||||
}
|
||||
|
||||
this.delete(text);
|
||||
this.insert(replacement, {matchIndentation: true});
|
||||
this.tabstops = stops;
|
||||
this.moveCaret(this.tabstops.shift());
|
||||
}
|
||||
|
||||
return !!expansion;
|
||||
}
|
||||
|
||||
get selectionStart() {
|
||||
return this.textarea.selectionStart;
|
||||
}
|
||||
set selectionStart(v) {
|
||||
this.textarea.selectionStart = v;
|
||||
}
|
||||
|
||||
get selectionEnd() {
|
||||
return this.textarea.selectionEnd;
|
||||
}
|
||||
set selectionEnd(v) {
|
||||
this.textarea.selectionEnd = v;
|
||||
}
|
||||
|
||||
get hasSelection() {
|
||||
return this.selectionStart != this.selectionEnd;
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.value.slice(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.textarea.value;
|
||||
}
|
||||
set value(v) {
|
||||
this.textarea.value = v;
|
||||
}
|
||||
|
||||
get indent() {
|
||||
return _.match(this.value, /^[\t ]+/m, _.DEFAULT_INDENT);
|
||||
}
|
||||
|
||||
get currentIndent() {
|
||||
var before = this.value.slice(0, this.selectionStart-1);
|
||||
return _.match(before, /^[\t ]*/mg, "", -1);
|
||||
}
|
||||
|
||||
// Current language at caret position
|
||||
get currentLanguage() {
|
||||
var node = this.getNode();
|
||||
node = node? node.parentNode : this.code;
|
||||
var lang = _.match(node.closest('[class*="language-"]').className, /language-(\w+)/, 1);
|
||||
return _.aliases[lang] || lang;
|
||||
}
|
||||
|
||||
// Get settings based on current language
|
||||
get context() {
|
||||
var lang = this.currentLanguage;
|
||||
return _.languages[lang] || _.languages.DEFAULT;
|
||||
}
|
||||
|
||||
setSelection(start, end) {
|
||||
if (start && typeof start === "object" && (start.start || start.end)) {
|
||||
end = start.end;
|
||||
start = start.start;
|
||||
}
|
||||
|
||||
let prevStart = this.selectionStart;
|
||||
let prevEnd = this.selectionEnd;
|
||||
|
||||
if (start !== undefined) {
|
||||
this.selectionStart = start;
|
||||
}
|
||||
|
||||
if (end !== undefined) {
|
||||
this.selectionEnd = end;
|
||||
}
|
||||
|
||||
// If there is a selection, and it's not the same as the previous selection, fire appropriate select event
|
||||
if (this.selectionStart !== this.selectionEnd && (prevStart !== this.selectionStart || prevEnd !== this.selectionEnd)) {
|
||||
this.textarea.dispatchEvent(new Event("select", {bubbles: true}));
|
||||
}
|
||||
}
|
||||
|
||||
update (force) {
|
||||
var code = this.value;
|
||||
|
||||
// If code ends in newline then browser "conveniently" trims it
|
||||
// but we want to see the new line we just inserted!
|
||||
// So we insert a zero-width space, which isn't trimmed
|
||||
if (/\n$/.test(this.value)) {
|
||||
code += "\u200b";
|
||||
}
|
||||
|
||||
if (!force && this.code.textContent === code && $(".token", this.code)) {
|
||||
// Already highlighted
|
||||
return;
|
||||
}
|
||||
|
||||
this.unobserve();
|
||||
this.code.textContent = code;
|
||||
|
||||
Prism.highlightElement(this.code);
|
||||
|
||||
this.observe();
|
||||
}
|
||||
|
||||
syncStyles() {
|
||||
// Copy pre metrics over to textarea
|
||||
var cs = getComputedStyle(this.pre);
|
||||
|
||||
// Copy styles from <pre> to textarea
|
||||
this.textarea.style.caretColor = cs.color;
|
||||
|
||||
var properties = /^(font|lineHeight)|[tT]abSize/gi;
|
||||
|
||||
for (var prop in cs) {
|
||||
if (cs[prop] && prop in this.textarea.style && properties.test(prop)) {
|
||||
this.wrapper.style[prop] = cs[prop];
|
||||
this.textarea.style[prop] = this.pre.style[prop] = "inherit";
|
||||
}
|
||||
}
|
||||
|
||||
// This is primarily for supporting the line-numbers plugin.
|
||||
this.textarea.style['padding-left'] = cs['padding-left'];
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
syncScroll() {
|
||||
if (this.pre.clientWidth === 0 && this.pre.clientHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pre.scrollTop = this.textarea.scrollTop;
|
||||
this.pre.scrollLeft = this.textarea.scrollLeft;
|
||||
}
|
||||
|
||||
beforeCaretIndex (until = "") {
|
||||
return this.value.lastIndexOf(until, this.selectionStart);
|
||||
}
|
||||
|
||||
afterCaretIndex (until = "") {
|
||||
return this.value.indexOf(until, this.selectionEnd);
|
||||
}
|
||||
|
||||
beforeCaret (until = "") {
|
||||
var index = this.beforeCaretIndex(until);
|
||||
|
||||
if (index === -1 || !until) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return this.value.slice(index, this.selectionStart);
|
||||
}
|
||||
|
||||
getLine(offset = this.selectionStart) {
|
||||
var value = this.value;
|
||||
var lf = "\n", cr = "\r";
|
||||
var start, end, char;
|
||||
|
||||
for (var start = this.selectionStart; char = value[start]; start--) {
|
||||
if (char === lf || char === cr || !start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var end = this.selectionStart; char = value[end]; end++) {
|
||||
if (char === lf || char === cr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {start, end};
|
||||
}
|
||||
|
||||
afterCaret(until = "") {
|
||||
var index = this.afterCaretIndex(until);
|
||||
|
||||
if (index === -1 || !until) {
|
||||
index = undefined;
|
||||
}
|
||||
|
||||
return this.value.slice(this.selectionEnd, index);
|
||||
}
|
||||
|
||||
setCaret(pos) {
|
||||
this.selectionStart = this.selectionEnd = pos;
|
||||
}
|
||||
|
||||
moveCaret(chars) {
|
||||
if (chars) {
|
||||
this.setCaret(this.selectionEnd + chars);
|
||||
}
|
||||
}
|
||||
|
||||
insert(text, {index} = {}) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.textarea.focus();
|
||||
|
||||
if (index === undefined) {
|
||||
// No specified index, insert in current caret position
|
||||
this.replace(text);
|
||||
}
|
||||
else {
|
||||
// Specified index, first move caret there
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
|
||||
this.selectionStart = this.selectionEnd = index;
|
||||
this.replace(text);
|
||||
|
||||
this.setSelection(
|
||||
start + (index < start? text.length : 0),
|
||||
end + (index <= end? text.length : 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace currently selected text
|
||||
replace (text) {
|
||||
var hadSelection = this.hasSelection;
|
||||
|
||||
this.insertText(text);
|
||||
|
||||
if (hadSelection) {
|
||||
// By default inserText places the caret at the end, losing any selection
|
||||
// What we want instead is the replaced text to be selected
|
||||
this.setSelection({start: this.selectionEnd - text.length});
|
||||
}
|
||||
}
|
||||
|
||||
// Set text between indexes and restore caret position
|
||||
set (text, {start, end} = {}) {
|
||||
var ss = this.selectionStart;
|
||||
var se = this.selectionEnd;
|
||||
|
||||
this.setSelection(start, end);
|
||||
|
||||
this.insertText(text);
|
||||
|
||||
this.setSelection(ss, se);
|
||||
}
|
||||
|
||||
insertText (text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.supportsExecCommand === true) {
|
||||
document.execCommand("insertText", false, text);
|
||||
}
|
||||
else if (_.supportsExecCommand === undefined) {
|
||||
// We still don't know if document.execCommand("insertText") is supported
|
||||
let value = this.value;
|
||||
|
||||
document.execCommand("insertText", false, text);
|
||||
|
||||
_.supportsExecCommand = value !== this.value;
|
||||
}
|
||||
|
||||
if (_.supportsExecCommand === false) {
|
||||
this.textarea.setRangeText(text, this.selectionStart, this.selectionEnd, "end");
|
||||
requestAnimationFrame(() => this.update());
|
||||
}
|
||||
|
||||
return _.supportsExecCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text with strings
|
||||
* @param before {String} The text to insert before
|
||||
* @param after {String} The text to insert after
|
||||
* @param start {Number} Character offset
|
||||
* @param end {Number} Character offset
|
||||
*/
|
||||
wrap ({before, after, start = this.selectionStart, end = this.selectionEnd} = {}) {
|
||||
var ss = this.selectionStart;
|
||||
var se = this.selectionEnd;
|
||||
var between = this.value.slice(start, end);
|
||||
|
||||
this.set(before + between + after, {start, end});
|
||||
|
||||
if (ss > start) {
|
||||
ss += before.length;
|
||||
}
|
||||
|
||||
if (se > start) {
|
||||
se += before.length;
|
||||
}
|
||||
|
||||
if (ss > end) {
|
||||
ss += after.length;
|
||||
}
|
||||
|
||||
if (se > end) {
|
||||
se += after.length;
|
||||
}
|
||||
|
||||
this.setSelection(ss, se);
|
||||
}
|
||||
|
||||
wrapSelection (o = {}) {
|
||||
var hadSelection = this.hasSelection;
|
||||
|
||||
this.replace(o.before + this.selection + o.after);
|
||||
|
||||
if (hadSelection) {
|
||||
if (o.outside) {
|
||||
// Do not include new text in selection
|
||||
this.selectionStart += o.before.length;
|
||||
this.selectionEnd -= o.after.length;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.moveCaret(-o.after.length);
|
||||
}
|
||||
}
|
||||
|
||||
toggleComment() {
|
||||
var comments = this.context.comments;
|
||||
|
||||
// Are we inside a comment?
|
||||
var node = this.getNode();
|
||||
var commentNode = node.parentNode.closest(".token.comment");
|
||||
|
||||
if (commentNode) {
|
||||
// Remove comment
|
||||
var start = this.getOffset(commentNode);
|
||||
var commentText = commentNode.textContent;
|
||||
|
||||
if (comments.singleline && commentText.indexOf(comments.singleline) === 0) {
|
||||
var end = start + commentText.length;
|
||||
this.set(this.value.slice(start + comments.singleline.length, end), {start, end});
|
||||
this.moveCaret(-comments.singleline.length);
|
||||
}
|
||||
else {
|
||||
comments = comments.multiline || comments;
|
||||
var end = start + commentText.length - comments[1].length;
|
||||
this.set(this.value.slice(start + comments[0].length, end), {start, end: end + comments[1].length});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not inside comment, add
|
||||
if (this.hasSelection) {
|
||||
comments = comments.multiline || comments;
|
||||
|
||||
this.wrapSelection({
|
||||
before: comments[0],
|
||||
after: comments[1]
|
||||
});
|
||||
}
|
||||
else {
|
||||
// No selection, wrap line
|
||||
// FIXME *inside indent*
|
||||
comments = comments.singleline? [comments.singleline, ""] : comments.multiline || comments;
|
||||
end = this.afterCaretIndex("\n");
|
||||
this.wrap({
|
||||
before: comments[0],
|
||||
after: comments[1],
|
||||
start: this.beforeCaretIndex("\n") + 1,
|
||||
end: end < 0? this.value.length : end
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
duplicateContent () {
|
||||
var before = this.beforeCaret("\n");
|
||||
var after = this.afterCaret("\n");
|
||||
var text = before + this.selection + after;
|
||||
|
||||
this.insert(text, {index: this.selectionStart - before.length});
|
||||
}
|
||||
|
||||
delete (characters, {forward, pos} = {}) {
|
||||
var i = characters = characters > 0? characters : (characters + "").length;
|
||||
|
||||
if (pos) {
|
||||
var selectionStart = this.selectionStart;
|
||||
this.selectionStart = pos;
|
||||
this.selectionEnd = pos + this.selectionEnd - selectionStart;
|
||||
}
|
||||
|
||||
while (i--) {
|
||||
document.execCommand(forward? "forwardDelete" : "delete");
|
||||
}
|
||||
|
||||
if (pos) {
|
||||
// Restore caret
|
||||
this.selectionStart = selectionStart - characters;
|
||||
this.selectionEnd = this.selectionEnd - pos + this.selectionStart;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text node at a given chracter offset
|
||||
*/
|
||||
getNode(offset = this.selectionStart, container = this.code) {
|
||||
var node, sum = 0;
|
||||
var walk = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
||||
|
||||
while (node = walk.nextNode()) {
|
||||
sum += node.data.length;
|
||||
|
||||
if (sum >= offset) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
// if here, offset is larger than maximum
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character offset of a given node in the highlighted source
|
||||
*/
|
||||
getOffset(node) {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(this.code);
|
||||
range.setEnd(node, 0);
|
||||
return range.toString().length;
|
||||
}
|
||||
|
||||
// Utility method to get regex matches
|
||||
static match(str, regex, def, index = 0) {
|
||||
if (typeof def === "number" && arguments.length === 3) {
|
||||
index = def;
|
||||
def = undefined;
|
||||
}
|
||||
|
||||
var match = str.match(regex);
|
||||
|
||||
if (index < 0) {
|
||||
index = match.length + index;
|
||||
}
|
||||
|
||||
return match? match[index] : def;
|
||||
}
|
||||
|
||||
static checkShortcut(shortcut, evt) {
|
||||
return shortcut.trim().split(/\s*\+\s*/).every(key => {
|
||||
switch (key) {
|
||||
case "Cmd": return evt[superKey];
|
||||
case "Ctrl": return evt.ctrlKey;
|
||||
case "Shift": return evt.shiftKey;
|
||||
case "Alt": return evt.altKey;
|
||||
default: return evt.key === key;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static registerLanguage(name, context, parent = _.languages.DEFAULT) {
|
||||
Object.setPrototypeOf(context, parent);
|
||||
return _.languages[name] = context;
|
||||
}
|
||||
|
||||
static matchIndentation(text, currentIndent) {
|
||||
// FIXME this assumes that text has no indentation of its own
|
||||
// to make this more generally useful beyond snippets, we should first
|
||||
// strip text's own indentation.
|
||||
text = text.replace(/\r?\n/g, "$&" + currentIndent);
|
||||
}
|
||||
|
||||
static adjustIndentation(text, {indentation, relative = true, indent = _.DEFAULT_INDENT}) {
|
||||
if (!relative) {
|
||||
// First strip min indentation
|
||||
var minIndent = text.match(_.regexp.gm`^(${indent})+`).sort()[0];
|
||||
|
||||
if (minIndent) {
|
||||
text.replace(_.regexp.gm`^${minIndent}`, "");
|
||||
}
|
||||
}
|
||||
|
||||
if (indentation < 0) {
|
||||
return text.replace(_.regexp.gm`^${indent}`, "");
|
||||
}
|
||||
else if (indentation > 0) { // Indent
|
||||
return text.replace(/^/gm, indent);
|
||||
}
|
||||
}
|
||||
|
||||
static create (source, ...args) {
|
||||
let ret = _.all.get(source);
|
||||
if (!ret) {
|
||||
ret = new _(source);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
// Static properties
|
||||
Object.assign(_, {
|
||||
all: new WeakMap(),
|
||||
ready,
|
||||
DEFAULT_INDENT: "\t",
|
||||
CARET_INDICATOR: /(^|[^\\])\$(\d+)/g,
|
||||
snippets: {
|
||||
"test": "Snippets work!",
|
||||
},
|
||||
pairs: {
|
||||
"(": ")",
|
||||
"[": "]",
|
||||
"{": "}",
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
"`": "`"
|
||||
},
|
||||
shortcuts: {
|
||||
"Cmd + /": function() {
|
||||
this.toggleComment();
|
||||
},
|
||||
"Ctrl + Shift + D": function() {
|
||||
this.duplicateContent();
|
||||
}
|
||||
},
|
||||
languages: {
|
||||
DEFAULT: {
|
||||
comments: {
|
||||
multiline: ["/*", "*/"]
|
||||
},
|
||||
snippets: {}
|
||||
}
|
||||
},
|
||||
// Map of Prism language ids and their canonical name
|
||||
aliases: (() => {
|
||||
var ret = {};
|
||||
var canonical = new WeakMap(Object.entries(Prism.languages).map(x => x.reverse()).reverse());
|
||||
|
||||
for (var id in Prism.languages) {
|
||||
var grammar = Prism.languages[id];
|
||||
|
||||
if (typeof grammar !== "function") {
|
||||
ret[id] = canonical.get(grammar);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
})(),
|
||||
|
||||
regexp: (() => {
|
||||
var escape = s => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||
var _regexp = (flags, strings, ...values) => {
|
||||
var pattern = strings[0] + values.map((v, i) => escape(v) + strings[i+1]).join("");
|
||||
return RegExp(pattern, flags);
|
||||
};
|
||||
var cache = {};
|
||||
|
||||
return new Proxy(_regexp.bind(_, ""), {
|
||||
get: (t, property) => {
|
||||
return t[property] || cache[property]
|
||||
|| (cache[property] = _regexp.bind(_, property));
|
||||
}
|
||||
});
|
||||
})()
|
||||
});
|
||||
|
||||
_.supportsExecCommand = document.execCommand? undefined : false;
|
||||
|
||||
$.ready().then(() => {
|
||||
$$(":not(.prism-live) > textarea.prism-live, :not(.prism-live) > pre.prism-live").forEach(source => _.create(source));
|
||||
});
|
||||
|
||||
})();
|
120
dev/prism/prism-onedark.css
Normal file
120
dev/prism/prism-onedark.css
Normal file
@ -0,0 +1,120 @@
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: var(--onedark-white);
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: var(--onedark-bg-light);
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: var(--onedark-gray);
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: var(--onedark-white);
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: var(--onedark-orange);
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: var(--onedark-yellow);
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: var(--onedark-purple);
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex {
|
||||
color: var(--onedark-green);
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: var(--onedark-red);
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: var(--onedark-purple);
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
|
BIN
dev/tabler-icons/tabler-icons.eot
Normal file
BIN
dev/tabler-icons/tabler-icons.eot
Normal file
Binary file not shown.
4
dev/tabler-icons/tabler-icons.min.css
vendored
Normal file
4
dev/tabler-icons/tabler-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2294
dev/tabler-icons/tabler-icons.svg
Normal file
2294
dev/tabler-icons/tabler-icons.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 4.2 MiB |
BIN
dev/tabler-icons/tabler-icons.ttf
Normal file
BIN
dev/tabler-icons/tabler-icons.ttf
Normal file
Binary file not shown.
BIN
dev/tabler-icons/tabler-icons.woff
Normal file
BIN
dev/tabler-icons/tabler-icons.woff
Normal file
Binary file not shown.
BIN
dev/tabler-icons/tabler-icons.woff2
Normal file
BIN
dev/tabler-icons/tabler-icons.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user