@@ -15,6 +15,7 @@ interface ResolvedMarkdownSnapshot {
1515 themeKey : string
1616 highlightEnabled : boolean
1717 escapeRawHtml : boolean
18+ defaultCodeBlockWrap : boolean
1819 partId : string | undefined
1920 cacheId : string
2021 version : string
@@ -96,6 +97,7 @@ interface MarkdownProps {
9697 size ?: "base" | "sm" | "tight"
9798 disableHighlight ?: boolean
9899 escapeRawHtml ?: boolean
100+ defaultCodeBlockWrap ?: boolean
99101 onRendered ?: ( ) => void
100102}
101103
@@ -105,32 +107,87 @@ export function Markdown(props: MarkdownProps) {
105107 let containerRef : HTMLDivElement | undefined
106108 let latestRequestKey = ""
107109 let cleanupLanguageListener : ( ( ) => void ) | undefined
110+ const codeBlockWrapOverrides = new Map < string , boolean > ( )
108111
109112 const notifyRendered = ( ) => {
110113 Promise . resolve ( ) . then ( ( ) => props . onRendered ?.( ) )
111114 }
112115
116+ const codeBlockWrapKey = ( codeBlock : HTMLElement ) : string | null => {
117+ const key = codeBlock . getAttribute ( "data-code-block-key" )
118+ if ( ! key ) {
119+ return null
120+ }
121+ return `${ resolved ( ) . cacheId } :${ key } `
122+ }
123+
124+ const applyCodeBlockWrapState = ( codeBlock : HTMLElement , enabled : boolean ) => {
125+ codeBlock . setAttribute ( "data-wrap-lines" , enabled ? "true" : "false" )
126+
127+ const button = codeBlock . querySelector < HTMLButtonElement > ( ".code-block-wrap" )
128+ if ( ! button ) {
129+ return
130+ }
131+
132+ const label = enabled ? t ( "markdown.codeBlock.wrap.disable" ) : t ( "markdown.codeBlock.wrap.enable" )
133+ button . classList . toggle ( "active" , enabled )
134+ button . setAttribute ( "aria-pressed" , enabled ? "true" : "false" )
135+ button . setAttribute ( "aria-label" , label )
136+ button . setAttribute ( "title" , label )
137+
138+ const text = button . querySelector ( ".wrap-text" )
139+ if ( text ) {
140+ text . textContent = label
141+ }
142+ }
143+
144+ const syncCodeBlockWrapStates = ( ) => {
145+ if ( ! containerRef ) {
146+ return
147+ }
148+
149+ const codeBlocks = containerRef . querySelectorAll < HTMLElement > ( ".markdown-code-block" )
150+ for ( const codeBlock of codeBlocks ) {
151+ const key = codeBlockWrapKey ( codeBlock )
152+ const defaultEnabled = codeBlock . getAttribute ( "data-wrap-lines" ) !== "false"
153+ const enabled = key ? ( codeBlockWrapOverrides . get ( key ) ?? defaultEnabled ) : defaultEnabled
154+ applyCodeBlockWrapState ( codeBlock , enabled )
155+ }
156+ }
157+
113158 const resolved = createMemo ( ( ) => {
114159 const part = props . part
115160 const rawText = typeof part . text === "string" ? part . text : ""
116161 const text = decodeHtmlEntitiesLocally ( rawText )
117162 const themeKey = Boolean ( props . isDark ) ? "dark" : "light"
118163 const highlightEnabled = ! props . disableHighlight
119164 const escapeRawHtml = Boolean ( props . escapeRawHtml )
165+ const defaultCodeBlockWrap = props . defaultCodeBlockWrap ?? true
120166 const partId = typeof part . id === "string" && part . id . length > 0 ? part . id : undefined
121167 const cacheId = resolvePartCacheId ( part , text )
122168 const version = resolvePartVersion ( part , text )
123- const requestKey = `${ cacheId } :${ themeKey } :${ highlightEnabled ? 1 : 0 } :${ escapeRawHtml ? 1 : 0 } :${ version } `
124- return { part, text, themeKey, highlightEnabled, escapeRawHtml, partId, cacheId, version, requestKey }
169+ const requestKey = `${ cacheId } :${ themeKey } :${ highlightEnabled ? 1 : 0 } :${ escapeRawHtml ? 1 : 0 } :${ defaultCodeBlockWrap ? 1 : 0 } :${ version } `
170+ return {
171+ part,
172+ text,
173+ themeKey,
174+ highlightEnabled,
175+ escapeRawHtml,
176+ defaultCodeBlockWrap,
177+ partId,
178+ cacheId,
179+ version,
180+ requestKey,
181+ }
125182 } )
126183
127184 const cacheHandle = useGlobalCache ( {
128185 instanceId : ( ) => props . instanceId ,
129186 sessionId : ( ) => props . sessionId ,
130187 scope : "markdown" ,
131188 cacheId : ( ) => {
132- const { cacheId, themeKey, highlightEnabled } = resolved ( )
133- return `${ cacheId } :${ themeKey } :${ highlightEnabled ? 1 : 0 } :${ resolved ( ) . escapeRawHtml ? 1 : 0 } `
189+ const { cacheId, themeKey, highlightEnabled, escapeRawHtml , defaultCodeBlockWrap } = resolved ( )
190+ return `${ cacheId } :${ themeKey } :${ highlightEnabled ? 1 : 0 } :${ escapeRawHtml ? 1 : 0 } : ${ defaultCodeBlockWrap ? 1 : 0 } `
134191 } ,
135192 version : ( ) => resolved ( ) . version ,
136193 } )
@@ -144,7 +201,7 @@ export function Markdown(props: MarkdownProps) {
144201 text : snapshot . text ,
145202 html : renderedHtml ,
146203 theme : snapshot . themeKey ,
147- mode : `${ snapshot . version } :${ snapshot . escapeRawHtml ? "escaped" : "raw" } ` ,
204+ mode : `${ snapshot . version } :${ snapshot . escapeRawHtml ? "escaped" : "raw" } : ${ snapshot . defaultCodeBlockWrap ? "wrap" : "nowrap" } ` ,
148205 }
149206 setHtml ( renderedHtml )
150207 if ( options ?. cache ?? true ) {
@@ -159,6 +216,7 @@ export function Markdown(props: MarkdownProps) {
159216 const rendered = await markdown . renderMarkdown ( snapshot . text , {
160217 suppressHighlight : ! snapshot . highlightEnabled ,
161218 escapeRawHtml : snapshot . escapeRawHtml ,
219+ defaultCodeBlockWrap : snapshot . defaultCodeBlockWrap ,
162220 } )
163221 const shouldCache = ! snapshot . highlightEnabled || ! markdown . hasPendingCodeHighlight ( snapshot . text )
164222
@@ -170,7 +228,7 @@ export function Markdown(props: MarkdownProps) {
170228 createEffect ( ( ) => {
171229 const snapshot = resolved ( )
172230 latestRequestKey = snapshot . requestKey
173- const cacheMode = `${ snapshot . version } :${ snapshot . escapeRawHtml ? "escaped" : "raw" } `
231+ const cacheMode = `${ snapshot . version } :${ snapshot . escapeRawHtml ? "escaped" : "raw" } : ${ snapshot . defaultCodeBlockWrap ? "wrap" : "nowrap" } `
174232
175233 const cacheMatches = ( cache : RenderCache | undefined ) => {
176234 if ( ! cache ) return false
@@ -202,9 +260,33 @@ export function Markdown(props: MarkdownProps) {
202260 } )
203261 } )
204262
263+ createEffect ( ( ) => {
264+ html ( )
265+ Promise . resolve ( ) . then ( syncCodeBlockWrapStates )
266+ } )
267+
205268 onMount ( ( ) => {
206269 const handleClick = async ( event : Event ) => {
207270 const target = event . target as HTMLElement
271+ const wrapButton = target . closest ( ".code-block-wrap" ) as HTMLButtonElement
272+ if ( wrapButton ) {
273+ event . preventDefault ( )
274+ const codeBlock = wrapButton . closest ( ".markdown-code-block" ) as HTMLElement | null
275+ if ( ! codeBlock ) {
276+ return
277+ }
278+
279+ const key = codeBlockWrapKey ( codeBlock )
280+ const current = codeBlock . getAttribute ( "data-wrap-lines" ) !== "false"
281+ const next = ! current
282+ if ( key ) {
283+ codeBlockWrapOverrides . set ( key , next )
284+ }
285+ applyCodeBlockWrapState ( codeBlock , next )
286+ props . onRendered ?.( )
287+ return
288+ }
289+
208290 const copyButton = target . closest ( ".code-block-copy" ) as HTMLButtonElement
209291
210292 if ( ! copyButton ) {
0 commit comments