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/</&lt;/g;
    $source =~ s/>/&gt;/g;

    my $urls = $self->_page_route_urls($page);
    my $form_action = $urls->{form_action} || '/';

    my $title = $page->as_hash->{title};
    $title =~ s/&/&amp;/g;
    $title =~ s/</&lt;/g;
    $title =~ s/>/&gt;/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 )