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
34 changes: 32 additions & 2 deletions src/DjotConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,43 @@ class DjotConverter
* @param bool $xhtml Whether to use XHTML-compatible output
* @param bool $warnings Whether to collect warnings during parsing
* @param bool $strict Whether to throw exceptions on parse errors
* @param \Djot\SafeMode|bool|null $safeMode Enable safe mode (true for defaults, SafeMode instance for custom config)
*/
public function __construct(bool $xhtml = false, bool $warnings = false, bool $strict = false)
{
public function __construct(
bool $xhtml = false,
bool $warnings = false,
bool $strict = false,
bool|SafeMode|null $safeMode = null,
) {
$this->collectWarnings = $warnings;
$this->strictMode = $strict;
$this->parser = new BlockParser($warnings, $strict);
$this->renderer = new HtmlRenderer($xhtml);

// Configure safe mode
if ($safeMode === true) {
$this->renderer->setSafeMode(SafeMode::defaults());
} elseif ($safeMode instanceof SafeMode) {
$this->renderer->setSafeMode($safeMode);
}
}

/**
* Enable or disable safe mode
*
* @param \Djot\SafeMode|bool|null $safeMode True for defaults, SafeMode for custom, null/false to disable
*/
public function setSafeMode(bool|SafeMode|null $safeMode): self
{
if ($safeMode === true) {
$this->renderer->setSafeMode(SafeMode::defaults());
} elseif ($safeMode instanceof SafeMode) {
$this->renderer->setSafeMode($safeMode);
} else {
$this->renderer->setSafeMode(null);
}

return $this;
}

/**
Expand Down
94 changes: 86 additions & 8 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use Djot\Node\Inline\Symbol;
use Djot\Node\Inline\Text;
use Djot\Node\Node;
use Djot\SafeMode;

/**
* Renders AST to HTML
Expand All @@ -52,6 +53,11 @@ class HtmlRenderer
{
protected bool $softBreakAsNewline = true;

/**
* Safe mode configuration (null = disabled)
*/
protected ?SafeMode $safeMode = null;

/**
* @var array<string, array<\Closure(\Djot\Event\RenderEvent): void>>
*/
Expand Down Expand Up @@ -99,6 +105,32 @@ public function __construct(protected bool $xhtml = false)
{
}

/**
* Enable safe mode with the given configuration
*/
public function setSafeMode(?SafeMode $safeMode): self
{
$this->safeMode = $safeMode;

return $this;
}

/**
* Get the current safe mode configuration
*/
public function getSafeMode(): ?SafeMode
{
return $this->safeMode;
}

/**
* Check if safe mode is enabled
*/
public function isSafeModeEnabled(): bool
{
return $this->safeMode !== null;
}

public function setSoftBreakAsNewline(bool $value): void
{
$this->softBreakAsNewline = $value;
Expand Down Expand Up @@ -312,6 +344,11 @@ protected function renderAttributesExcluding(Node $node, array $exclude): string
return '';
}

// Filter dangerous attributes in safe mode
if ($this->safeMode !== null) {
$attrs = $this->safeMode->filterAttributes($attrs);
}

// Sort attributes: id first, then others in source order
uksort($attrs, function (string $a, string $b): int {
if ($a === 'id') {
Expand Down Expand Up @@ -625,6 +662,11 @@ protected function renderLink(Link $node): string
$href = $node->getDestination();
$title = $node->getTitle();

// Sanitize URL in safe mode
if ($this->safeMode !== null && $href !== null) {
$href = $this->safeMode->sanitizeUrl($href);
}

$html = '<a';
// Only output href if destination is set (even if empty)
if ($href !== null) {
Expand All @@ -642,10 +684,15 @@ protected function renderImage(Image $node): string
{
$attrs = $this->renderAttributes($node);
$alt = $this->escape($node->getAlt());
$src = $this->escape($node->getSource());
$src = $node->getSource();
$title = $node->getTitle();

$html = '<img alt="' . $alt . '" src="' . $src . '"';
// Sanitize URL in safe mode
if ($this->safeMode !== null) {
$src = $this->safeMode->sanitizeUrl($src);
}

$html = '<img alt="' . $alt . '" src="' . $this->escape($src) . '"';
if ($title !== null) {
$html .= ' title="' . $this->escape($title) . '"';
}
Expand Down Expand Up @@ -720,6 +767,11 @@ protected function renderAttributes(Node $node): string
return '';
}

// Filter dangerous attributes in safe mode
if ($this->safeMode !== null) {
$attrs = $this->safeMode->filterAttributes($attrs);
}

// Sort attributes: id first, then others in source order
uksort($attrs, function (string $a, string $b): int {
if ($a === 'id') {
Expand Down Expand Up @@ -768,21 +820,47 @@ protected function escapeAttribute(string $text): string
protected function renderRawBlock(RawBlock $node): string
{
// Only output if format is HTML
if ($node->getFormat() === 'html') {
return $node->getContent() . "\n";
if ($node->getFormat() !== 'html') {
return '';
}

return '';
$content = $node->getContent();

// Handle raw HTML according to safe mode
if ($this->safeMode !== null) {
$mode = $this->safeMode->getRawHtmlMode();
if ($mode === SafeMode::RAW_HTML_STRIP) {
return '';
}
if ($mode === SafeMode::RAW_HTML_ESCAPE) {
return $this->escape($content) . "\n";
}
}

return $content . "\n";
}

protected function renderRawInline(RawInline $node): string
{
// Only output if format is HTML
if ($node->getFormat() === 'html') {
return $node->getContent();
if ($node->getFormat() !== 'html') {
return '';
}

return '';
$content = $node->getContent();

// Handle raw HTML according to safe mode
if ($this->safeMode !== null) {
$mode = $this->safeMode->getRawHtmlMode();
if ($mode === SafeMode::RAW_HTML_STRIP) {
return '';
}
if ($mode === SafeMode::RAW_HTML_ESCAPE) {
return $this->escape($content);
}
}

return $content;
}

protected function renderDefinitionList(DefinitionList $node): string
Expand Down
Loading