Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/Web/App.pm view on Meta::CPAN
};
}
# _page_response($page, $mode)
# Builds a page response for edit, render, or source mode.
# Input: page document object and mode string.
# Output: response array reference.
sub _page_response {
my ( $self, $page, $mode ) = @_;
my $source = $page->{meta}{raw_instruction} || $page->canonical_instruction;
if ( $mode eq 'source' ) {
return _no_editor_response() if $self->_editor_disabled;
return [ 200, 'text/plain; charset=utf-8', $source ];
}
if ( $mode eq 'render' ) {
return [ 200, 'text/html; charset=utf-8', $self->_render_page_html( $page, 'render' ) ];
}
return _no_editor_response() if $self->_editor_disabled;
return [ 200, 'text/html; charset=utf-8', $self->_edit_html($page) ];
}
# _edit_html($page)
# Renders the browser edit/source view for a page.
# Input: page document object.
# Output: HTML string.
sub _edit_html {
my ( $self, $page ) = @_;
my $raw_source = $page->{meta}{raw_instruction} || $page->canonical_instruction;
my $source = $raw_source;
$source =~ s/&/&/g;
$source =~ s/</</g;
$source =~ s/>/>/g;
my $urls = $self->_page_route_urls($page);
my $form_action = $urls->{form_action} || '/';
my $title = $page->as_hash->{title};
$title =~ s/&/&/g;
$title =~ s/</</g;
$title =~ s/>/>/g;
my $html = <<'HTML';
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>__TITLE__</title>
<style>
body { margin: 0; font-family: Georgia, serif; background: #f5efe2; color: #1f2a2e; }
main { max-width: 980px; margin: 32px auto; background: #fffef9; border: 1px solid #ddd3c2; padding: 24px; }
.editor-stack {
position: relative;
min-height: 520px;
border: 1px solid #2a2f36;
background: #1f2328;
overflow: hidden;
}
.editor-overlay,
.instruction-block-editor {
display: block;
width: 100%;
min-height: 520px;
box-sizing: border-box;
margin: 0;
padding: 12px;
font-family: Menlo, Consolas, "Courier New", monospace;
font-size: 14px;
line-height: 21px;
white-space: pre;
word-break: normal;
overflow-wrap: normal;
overflow: auto;
tab-size: 4;
letter-spacing: 0;
}
.editor-overlay-viewport {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
background: transparent;
}
.editor-overlay {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
color: #e6edf3;
background: transparent;
unicode-bidi: plaintext;
direction: ltr;
will-change: transform;
}
.instruction-block-editor {
position: relative;
z-index: 1;
min-height: 220px;
color: transparent;
border: 0;
resize: vertical;
background: transparent;
caret-color: #f8f8f2;
outline: none;
-webkit-text-fill-color: transparent;
unicode-bidi: plaintext;
direction: ltr;
overflow: auto;
scrollbar-gutter: stable both-edges;
}
.instruction-block-editor::selection {
background: rgba(121, 192, 255, 0.35);
-webkit-text-fill-color: transparent;
}
.instruction-source {
display: none;
}
.editor-blocks {
display: grid;
gap: 16px;
}
.editor-block {
display: grid;
gap: 8px;
}
.editor-block-label {
color: #9da7b3;
font: 600 12px/1.4 Menlo, Consolas, "Courier New", monospace;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.editor-hint {
color: #727b84;
font-size: 12px;
margin-bottom: 12px;
}
.tok-directive { color: #ffd866; font-weight: normal; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.tok-separator { color: #5c6370; }
.tok-html { color: #78dce8; }
.tok-tag { color: #ff7ab2; }
.tok-attr { color: #ffcf6a; }
.tok-value { color: #a9dc76; }
.tok-css { color: #78dce8; }
.tok-js { color: #ab9df2; }
lib/Developer/Dashboard/Web/App.pm view on Meta::CPAN
if (editor.selectionStart !== editor.selectionEnd) return false;
const caret = editor.selectionStart;
const value = editor.value;
const lineStart = value.lastIndexOf('\n', Math.max(caret - 1, 0) - 1) + 1;
const lineEndAt = value.indexOf('\n', caret);
const lineEnd = lineEndAt >= 0 ? lineEndAt : value.length;
const line = value.slice(lineStart, lineEnd);
if (line !== ':---' || caret !== lineEnd) return false;
const textBefore = value.slice(0, lineStart);
const textAfter = value.slice(lineEnd);
const nextDirective = ddNextDirectiveForContext(textBefore, value);
let replacement = ':--------------------------------------------------------------------------------:';
let nextCaret = lineStart + replacement.length;
if (nextDirective) {
replacement += '\n' + nextDirective;
nextCaret += 1 + nextDirective.length;
}
editor.value = textBefore + replacement + textAfter;
editor.setSelectionRange(nextCaret, nextCaret);
return true;
}
function ddSyncEditorOverlay(editor, highlight) {
highlight.style.minWidth = Math.max(editor.scrollWidth, editor.clientWidth) + 'px';
highlight.style.minHeight = Math.max(editor.scrollHeight, editor.clientHeight) + 'px';
highlight.style.transform = 'translate(' + (-editor.scrollLeft) + 'px, ' + (-editor.scrollTop) + 'px)';
}
function ddAutoResizeEditor(editor) {
editor.style.height = 'auto';
editor.style.height = Math.max(editor.scrollHeight, 48) + 'px';
}
function ddRenderEditor(editor, highlight) {
highlight.innerHTML = ddOverlayHtml(editor.value);
ddAutoResizeEditor(editor);
ddSyncEditorOverlay(editor, highlight);
}
function ddBlockLabel(text, index) {
const firstLine = String(text || '').split('\n')[0] || '';
const directive = ddDirectiveName(firstLine);
return directive || ('SECTION ' + (index + 1));
}
function ddFocusBlock(editor) {
editor.focus();
const caret = editor.value.length;
editor.setSelectionRange(caret, caret);
}
function ddCreateEditorBlock(text, index) {
const wrapper = document.createElement('div');
wrapper.className = 'editor-block';
const label = document.createElement('div');
label.className = 'editor-block-label';
label.textContent = ddBlockLabel(text, index);
wrapper.appendChild(label);
const stack = document.createElement('div');
stack.className = 'editor-stack';
wrapper.appendChild(stack);
const viewport = document.createElement('div');
viewport.className = 'editor-overlay-viewport';
viewport.setAttribute('aria-hidden', 'true');
stack.appendChild(viewport);
const highlight = document.createElement('pre');
highlight.className = 'editor-overlay';
viewport.appendChild(highlight);
const editor = document.createElement('textarea');
editor.className = 'instruction-block-editor';
editor.wrap = 'off';
editor.spellcheck = false;
editor.autocapitalize = 'off';
editor.autocomplete = 'off';
editor.autocorrect = 'off';
editor.value = text;
stack.appendChild(editor);
function sync() {
label.textContent = ddBlockLabel(editor.value, index);
ddComposeInstruction();
ddRenderEditor(editor, highlight);
}
editor.addEventListener('input', function() {
ddApplyDirectiveAssist(editor);
sync();
});
editor.addEventListener('scroll', function() {
ddSyncEditorOverlay(editor, highlight);
});
editor.addEventListener('keydown', function(event) {
if (event.key !== 'Tab' || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return;
event.preventDefault();
ddInsertBlockAfter(wrapper);
});
sync();
return wrapper;
}
function ddNextBlockSeed(afterNode) {
const editors = Array.prototype.slice.call(ddBlocks.querySelectorAll('.instruction-block-editor'));
const currentEditor = afterNode ? afterNode.querySelector('.instruction-block-editor') : null;
const fullText = editors.map(function(node) { return node.value; }).join('\n' + ddLegacySep + '\n');
let textBefore = '';
if (currentEditor) {
const index = editors.indexOf(currentEditor);
textBefore = editors.slice(0, index + 1).map(function(node) { return node.value; }).join('\n' + ddLegacySep + '\n');
}
return ddNextDirectiveForContext(textBefore, fullText);
}
function ddRefreshBlockLabels() {
Array.prototype.slice.call(ddBlocks.querySelectorAll('.editor-block')).forEach(function(block, index) {
const editor = block.querySelector('.instruction-block-editor');
const label = block.querySelector('.editor-block-label');
label.textContent = ddBlockLabel(editor.value, index);
});
}
function ddInsertBlockAfter(currentBlock) {
const seed = ddNextBlockSeed(currentBlock);
const newBlock = ddCreateEditorBlock(seed, ddBlocks.querySelectorAll('.editor-block').length);
if (currentBlock && currentBlock.nextSibling) {
ddBlocks.insertBefore(newBlock, currentBlock.nextSibling);
} else {
ddBlocks.appendChild(newBlock);
}
ddRefreshBlockLabels();
ddComposeInstruction();
ddFocusBlock(newBlock.querySelector('.instruction-block-editor'));
}
function ddLoadBlocks(text) {
ddBlocks.innerHTML = '';
const sections = ddSplitInstruction(text);
if (!sections.length) sections.push('TITLE: Untitled');
sections.forEach(function(section, index) {
ddBlocks.appendChild(ddCreateEditorBlock(section, index));
});
ddRefreshBlockLabels();
ddComposeInstruction();
}
ddForm.addEventListener('focusout', function() {
window.setTimeout(function() {
if (ddForm.contains(document.activeElement)) return;
ddMode.value = 'edit';
ddComposeInstruction();
ddForm.submit();
}, 0);
});
ddForm.addEventListener('submit', function() {
ddComposeInstruction();
});
if (ddPlayButton) {
ddPlayButton.addEventListener('click', function() {
ddMode.value = 'render';
ddComposeInstruction();
ddForm.submit();
});
}
window.addEventListener('resize', function() {
Array.prototype.slice.call(ddBlocks.querySelectorAll('.editor-block')).forEach(function(block) {
const editor = block.querySelector('.instruction-block-editor');
const highlight = block.querySelector('.editor-overlay');
ddAutoResizeEditor(editor);
ddSyncEditorOverlay(editor, highlight);
});
});
ddSource.value = __SOURCE_JSON__;
ddLoadBlocks(ddSource.value);
</script>
</body>
</html>
HTML
$html =~ s/__TITLE__/$title/g;
$html =~ s/__TOP_CHROME__/$self->_top_chrome_html( $page, \%$urls )/ge;
$html =~ s/__SOURCE__/$source/g;
$html =~ s/__SOURCE_JSON__/_json_for_inline_script($raw_source)/ge;
$html =~ s/__FORM_ACTION__/$form_action/g;
return $html;
}
# _json_for_inline_script($text)
# Encodes text as JSON for inline script assignment without allowing HTML parser breakouts.
# Input: raw text string.
# Output: JSON string literal safe for inclusion inside a <script> block.
sub _json_for_inline_script {
my ($text) = @_;
my $json = json_encode( defined $text ? $text : '' );
$json =~ s/</\\u003c/g;
$json =~ s/>/\\u003e/g;
$json =~ s/&/\\u0026/g;
return $json;
}
# _highlight_instruction_html($source)
# Generates the initial syntax-coloured editor HTML from bookmark source text.
# Input: canonical bookmark instruction text.
# Output: highlighted HTML string for the editable source area.
sub _highlight_instruction_html {
my ( $self, $source ) = @_;
my %state = ( section => '', html_mode => '' );
return join "\n", map { $self->_highlight_editor_line( $_, \%state ) } split /\n/, ( $source // '' ), -1;
}
# _editor_overlay_html($source)
# Generates the browser overlay HTML while preserving the textarea's final blank line geometry.
# Input: canonical bookmark instruction text.
# Output: highlighted HTML string with a trailing sentinel when the source ends in a newline.
sub _editor_overlay_html {
my ( $self, $source ) = @_;
my $html = $self->_highlight_instruction_html($source);
$html .= ' ' if defined $source && $source =~ /\n\z/;
return $html;
}
# _highlight_editor_line($line, $state)
# Highlights a single bookmark editor line while preserving exact layout.
# Input: raw line text and mutable parser state hash reference.
# Output: highlighted HTML fragment for that line.
sub _highlight_editor_line {
my ( $self, $line, $state ) = @_;
if ( $line eq ':--------------------------------------------------------------------------------:' ) {
return qq{<span class="tok-separator">} . _escape_html($line) . qq{</span>};
}
if ( $line =~ /^([A-Za-z][A-Za-z0-9.]*:)(\s*)(.*)\z/ ) {
my ( $directive, $space, $rest ) = ( $1, $2, $3 );
$state->{section} = uc substr( $directive, 0, -1 );
$state->{html_mode} = '';
return qq{<span class="tok-directive">} . _escape_html($directive) . qq{</span>}
. _escape_html($space)
. $self->_highlight_section_text( $rest, $state );
}
return $self->_highlight_section_text( $line, $state );
}
# _highlight_section_text($text, $state)
# Dispatches line highlighting based on the active bookmark section.
# Input: raw line text and mutable parser state hash reference.
# Output: highlighted HTML fragment.
sub _highlight_section_text {
my ( $self, $text, $state ) = @_;
my $section = $state->{section} || '';
return $self->_highlight_perl_text($text) if $section =~ /^CODE\d+$/;
return $self->_highlight_html_text( $text, $state ) if $section eq 'HTML';
return $self->_highlight_note_text($text) if $section eq 'STASH' || $section eq 'NOTE';
return _escape_html($text);
}
# _highlight_note_text($text)
# Highlights TT-style placeholders inside simple text sections.
# Input: raw section line text.
# Output: highlighted HTML fragment.
sub _highlight_note_text {
my ( $self, $text ) = @_;
my $html = _escape_html($text);
$html =~ s{(\[%[\s\S]*?%\])}{<span class="tok-note">$1</span>}g;
return $html;
}
# _highlight_html_text($text, $state)
# Highlights HTML section text with HTML, CSS, and JavaScript awareness.
# Input: raw section line text and mutable parser state hash reference.
# Output: highlighted HTML fragment.
sub _highlight_html_text {
my ( $self, $text, $state ) = @_;
my $line = $text;
my $out = '';
while ( length $line ) {
if ( ( $state->{html_mode} || '' ) eq 'script' ) {
( run in 0.870 second using v1.01-cache-2.11-cpan-df04353d9ac )