Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/linters/.editorconfig-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"^code_coverage/",
"^composer\\.lock$",
"^runtime/",
"^tests/Support/PrintR/server-request\\.txt$",
"^tests/runtime/",
"^vendor/",
"phpstan-baseline\\.neon$",
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 0.1.2 Under development

- feat(log): render multi-line log messages with preserved whitespace, `print_r()` dumps as a tree, and `var_export()` values highlighted.

## 0.1.1 October 28, 2026

- fix(event): skip `InstrumentationGuard` trace frames on Windows by joining the skip path with `DIRECTORY_SEPARATOR`.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"ext-ctype": "*",
"ext-intl": "*",
"ext-mbstring": "*",
"ext-tokenizer": "*",
"ui-awesome/html": "^0.6",
"ui-awesome/html-core-component": "^0.4",
"ui-awesome/html-helper": "^0.7",
Expand Down
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions resources/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -3471,6 +3471,138 @@
}
}

/**
* Multi-line log message: the text keeps its line breaks and indentation
* but reads like the single-line messages around it.
*/
.yii-debug-log-text {
margin: 0;
padding: 0;
background: none;
color: inherit;
font-family: var(--yii-debug-font-mono);
font-size: var(--yii-debug-fs-xs);
line-height: 1.6;
font-variant-ligatures: none;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

/**
* print_r() dump tree: an open root block over collapsed nested blocks,
* sharing the tokens of the queue payload tree. A highlighted
* var_export() message shares the box.
*/
.yii-debug-log-dump,
.yii-debug-log-export pre {
padding: var(--yii-debug-space-2) var(--yii-debug-space-3);
border-radius: var(--yii-debug-radius-sm);
background: color-mix(
in srgb,
var(--yii-debug-panel-text),
transparent 96%
);
font-family: var(--yii-debug-font-mono);
font-size: var(--yii-debug-fs-xs);
line-height: 1.6;
font-variant-ligatures: none;
}

.yii-debug-log-dump-summary,
.yii-debug-log-dump-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 2px var(--yii-debug-space-2);
padding-left: calc(1ch + var(--yii-debug-space-2));
}

/* The marker sits in the padding so it never wraps apart from the label. */
.yii-debug-log-dump-summary {
position: relative;
cursor: pointer;
list-style: none;

&::-webkit-details-marker {
display: none;
}

&::before {
position: absolute;
left: 0;
content: "\25B8";
color: var(--yii-debug-panel-muted);
}
}

:is(.yii-debug-log-dump, .yii-debug-log-dump-node)[open]
> .yii-debug-log-dump-summary::before {
transform: rotate(90deg);
}

.yii-debug-log-dump-entries {
margin-left: 0.5ch;
padding-left: calc(0.5ch + var(--yii-debug-space-2));
border-left: 1px dashed
color-mix(in srgb, var(--yii-debug-panel-text), transparent 80%);
}

.yii-debug-log-dump-key {
font-weight: 700;
white-space: pre-wrap;
overflow-wrap: anywhere;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

&::after {
content: ":";
color: var(--yii-debug-panel-muted);
}
}

.yii-debug-log-dump-value {
flex: 1 1 16ch;
min-width: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.yii-debug-log-dump-kind,
.yii-debug-log-dump-visibility,
.yii-debug-log-dump-recursion {
padding: 0 6px;
border-radius: 3px;
background: color-mix(
in srgb,
var(--yii-debug-panel-text),
transparent 92%
);
letter-spacing: 0.05em;
}

.yii-debug-log-dump-kind,
.yii-debug-log-dump-recursion {
text-transform: uppercase;
}

.yii-debug-log-dump-kind,
.yii-debug-log-dump-visibility,
.yii-debug-log-dump-count,
.yii-debug-log-dump-prefix,
.yii-debug-log-dump-empty {
color: var(--yii-debug-panel-muted);
}

.yii-debug-log-dump-visibility,
.yii-debug-log-dump-empty {
font-style: italic;
cursor: help;
}

.yii-debug-log-dump-recursion {
background: color-mix(in srgb, var(--yii-debug-warning), transparent 86%);
color: var(--yii-debug-warning);
cursor: help;
}

/* Inline helper utilities. */
.yii-debug-muted {
color: var(--yii-debug-panel-muted);
Expand Down
32 changes: 27 additions & 5 deletions src/Helper/PhpHighlighter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,39 @@ final class PhpHighlighter
/**
* Highlights a value as the PHP expression that would recreate it.
*
* The expression is built here and handed straight to PHP's highlighter, which escapes it, so the result carries no
* markup from the captured value.
*
* The `<pre>` wrapper scrolls horizontally, so it carries `tabindex="0"` to stay reachable by keyboard.
* The expression is built here with {@see Dump::export()} and handed to {@see self::highlightCode()}, so the result
* carries no markup from the captured value.
*
* @param mixed $value JSON-safe value to render.
*
* @return string Highlighted markup in a focusable `<pre>`, without the opening tag PHP needs to recognize the
* snippet.
*/
public static function highlight(mixed $value): string
{
return self::highlightCode(Dump::export($value));
}

/**
* Highlights PHP source text, such as the `var_export()` output carried by a log message.
*
* PHP's highlighter escapes the whole source, so markup carried by the text renders as text and never as markup.
* The source is only highlighted, never evaluated.
*
* The `<pre>` wrapper scrolls horizontally, so it carries `tabindex="0"` to stay reachable by keyboard.
*
* Usage example:
* ```php
* $html = PhpHighlighter::highlightCode("array (\n 'id' => 7,\n)");
* // '<pre tabindex="0"><code style="color: #000000"><span style="color: #007700">array (' . "\n" . ' </span>...'
* ```
*
* @param string $code PHP source of an expression, without the opening tag.
*
* @return string Highlighted markup in a focusable `<pre>`, without the opening tag PHP needs to recognize the
* snippet.
*/
public static function highlightCode(string $code): string
{
return (string) preg_replace(
[
Expand All @@ -34,7 +56,7 @@ public static function highlight(mixed $value): string
'~^<pre>~',
],
['$1', '$1', '<pre tabindex="0">'],
highlight_string("<?php\n" . Dump::export($value), true),
highlight_string("<?php\n" . $code, true),
);
}
}
52 changes: 52 additions & 0 deletions src/Helper/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace PHPForge\Debug\Helper;

use UIAwesome\Html\Helper\Encode;

use function is_string;
use function mb_strtolower;
use function parse_url;
Expand All @@ -16,6 +18,11 @@
*/
final class Text
{
/**
* Line break sequences, `\r\n` first so that it reads as one line break, as the HTML parser reads it.
*/
private const array LINE_BREAKS = ["\r\n", "\r", "\n"];

/**
* Converts a CamelCase name into a lowercase id with `-` word separators, matching the Yii inflector semantics.
*
Expand All @@ -30,6 +37,51 @@ public static function camel2id(string $name): string
return mb_strtolower(trim(str_replace('_', '-', $replaced), '-'), 'UTF-8');
}

/**
* Writes each line break of HTML markup as the `&#10;` character reference, leaving the markup itself unchanged.
*
* Use it for markup that is already escaped and carries line breaks only in its text, such as
* {@see PhpHighlighter::highlightCode()} output; {@see self::preserveLineBreaks()} escapes raw text first. `\r\n`
* and a lone `\r` become one line break, as the HTML parser reads them.
*
* Usage example:
* ```php
* $html = Text::preserveHtmlLineBreaks("<span>'a\r\nb'</span>");
* // "<span>'a&#10;b'</span>"
* ```
*
* @param string $html Escaped HTML markup.
*
* @return string Markup without raw line breaks.
*/
public static function preserveHtmlLineBreaks(string $html): string
{
return str_replace(self::LINE_BREAKS, '&#10;', $html);
}

/**
* Escapes text for HTML content and writes each of its line breaks as the `&#10;` character reference.
*
* The UI Awesome block builders other than `<pre>` collapse consecutive line breaks in their rendered output, which
* drops the blank lines of a multi-line value rendered inside them; a character reference survives that
* normalization and still renders as a line break under `white-space: pre-wrap`. `\r\n` and a lone `\r` become one
* line break, as the HTML parser reads them.
*
* Usage example:
* ```php
* $html = Pre::tag()->html(Text::preserveLineBreaks("a\n\n<b>"))->render();
* // "<pre>\na&#10;&#10;&lt;b&gt;\n</pre>"
* ```
*
* @param string $text Raw text to render.
*
* @return string Escaped HTML content without raw line breaks.
*/
public static function preserveLineBreaks(string $text): string
{
return self::preserveHtmlLineBreaks(Encode::content($text));
}

/**
* Strips the authority from a captured URL for display, retaining its path, non-empty query, and fragment.
*
Expand Down
Loading
Loading