4444use Djot \Node \Inline \Symbol ;
4545use Djot \Node \Inline \Text ;
4646use Djot \Node \Node ;
47+ use Djot \SafeMode ;
4748
4849/**
4950 * Renders AST to HTML
@@ -52,6 +53,11 @@ class HtmlRenderer
5253{
5354 protected bool $ softBreakAsNewline = true ;
5455
56+ /**
57+ * Safe mode configuration (null = disabled)
58+ */
59+ protected ?SafeMode $ safeMode = null ;
60+
5561 /**
5662 * @var array<string, array<\Closure(\Djot\Event\RenderEvent): void>>
5763 */
@@ -99,6 +105,32 @@ public function __construct(protected bool $xhtml = false)
99105 {
100106 }
101107
108+ /**
109+ * Enable safe mode with the given configuration
110+ */
111+ public function setSafeMode (?SafeMode $ safeMode ): self
112+ {
113+ $ this ->safeMode = $ safeMode ;
114+
115+ return $ this ;
116+ }
117+
118+ /**
119+ * Get the current safe mode configuration
120+ */
121+ public function getSafeMode (): ?SafeMode
122+ {
123+ return $ this ->safeMode ;
124+ }
125+
126+ /**
127+ * Check if safe mode is enabled
128+ */
129+ public function isSafeModeEnabled (): bool
130+ {
131+ return $ this ->safeMode !== null ;
132+ }
133+
102134 public function setSoftBreakAsNewline (bool $ value ): void
103135 {
104136 $ this ->softBreakAsNewline = $ value ;
@@ -312,6 +344,11 @@ protected function renderAttributesExcluding(Node $node, array $exclude): string
312344 return '' ;
313345 }
314346
347+ // Filter dangerous attributes in safe mode
348+ if ($ this ->safeMode !== null ) {
349+ $ attrs = $ this ->safeMode ->filterAttributes ($ attrs );
350+ }
351+
315352 // Sort attributes: id first, then others in source order
316353 uksort ($ attrs , function (string $ a , string $ b ): int {
317354 if ($ a === 'id ' ) {
@@ -625,6 +662,11 @@ protected function renderLink(Link $node): string
625662 $ href = $ node ->getDestination ();
626663 $ title = $ node ->getTitle ();
627664
665+ // Sanitize URL in safe mode
666+ if ($ this ->safeMode !== null && $ href !== null ) {
667+ $ href = $ this ->safeMode ->sanitizeUrl ($ href );
668+ }
669+
628670 $ html = '<a ' ;
629671 // Only output href if destination is set (even if empty)
630672 if ($ href !== null ) {
@@ -642,10 +684,15 @@ protected function renderImage(Image $node): string
642684 {
643685 $ attrs = $ this ->renderAttributes ($ node );
644686 $ alt = $ this ->escape ($ node ->getAlt ());
645- $ src = $ this -> escape ( $ node ->getSource () );
687+ $ src = $ node ->getSource ();
646688 $ title = $ node ->getTitle ();
647689
648- $ html = '<img alt=" ' . $ alt . '" src=" ' . $ src . '" ' ;
690+ // Sanitize URL in safe mode
691+ if ($ this ->safeMode !== null ) {
692+ $ src = $ this ->safeMode ->sanitizeUrl ($ src );
693+ }
694+
695+ $ html = '<img alt=" ' . $ alt . '" src=" ' . $ this ->escape ($ src ) . '" ' ;
649696 if ($ title !== null ) {
650697 $ html .= ' title=" ' . $ this ->escape ($ title ) . '" ' ;
651698 }
@@ -720,6 +767,11 @@ protected function renderAttributes(Node $node): string
720767 return '' ;
721768 }
722769
770+ // Filter dangerous attributes in safe mode
771+ if ($ this ->safeMode !== null ) {
772+ $ attrs = $ this ->safeMode ->filterAttributes ($ attrs );
773+ }
774+
723775 // Sort attributes: id first, then others in source order
724776 uksort ($ attrs , function (string $ a , string $ b ): int {
725777 if ($ a === 'id ' ) {
@@ -768,21 +820,47 @@ protected function escapeAttribute(string $text): string
768820 protected function renderRawBlock (RawBlock $ node ): string
769821 {
770822 // Only output if format is HTML
771- if ($ node ->getFormat () = == 'html ' ) {
772- return $ node -> getContent () . "\n" ;
823+ if ($ node ->getFormat () ! == 'html ' ) {
824+ return '' ;
773825 }
774826
775- return '' ;
827+ $ content = $ node ->getContent ();
828+
829+ // Handle raw HTML according to safe mode
830+ if ($ this ->safeMode !== null ) {
831+ $ mode = $ this ->safeMode ->getRawHtmlMode ();
832+ if ($ mode === SafeMode::RAW_HTML_STRIP ) {
833+ return '' ;
834+ }
835+ if ($ mode === SafeMode::RAW_HTML_ESCAPE ) {
836+ return $ this ->escape ($ content ) . "\n" ;
837+ }
838+ }
839+
840+ return $ content . "\n" ;
776841 }
777842
778843 protected function renderRawInline (RawInline $ node ): string
779844 {
780845 // Only output if format is HTML
781- if ($ node ->getFormat () = == 'html ' ) {
782- return $ node -> getContent () ;
846+ if ($ node ->getFormat () ! == 'html ' ) {
847+ return '' ;
783848 }
784849
785- return '' ;
850+ $ content = $ node ->getContent ();
851+
852+ // Handle raw HTML according to safe mode
853+ if ($ this ->safeMode !== null ) {
854+ $ mode = $ this ->safeMode ->getRawHtmlMode ();
855+ if ($ mode === SafeMode::RAW_HTML_STRIP ) {
856+ return '' ;
857+ }
858+ if ($ mode === SafeMode::RAW_HTML_ESCAPE ) {
859+ return $ this ->escape ($ content );
860+ }
861+ }
862+
863+ return $ content ;
786864 }
787865
788866 protected function renderDefinitionList (DefinitionList $ node ): string
0 commit comments