s?d--:d++,a.__u|=4))):e.__k[o]=null;if(b)for(o=0;o
(p?1:0)){for(n=r-1,o=r+1;n>=0||o
-`};fe.fence=function(e,t,r,u,n){let o=e[t],a=o.info?_e(o.info).trim():"",i="",s="";if(a){let p=a.split(/(\s+)/g);i=p[0],s=p.slice(2).join("")}let c;if(r.highlight?c=r.highlight(o.content,i,s)||ke(o.content):c=ke(o.content),c.indexOf(""+ke(e[t].content)+`
-`}return`${c}
+(()=>{var si=Object.create;var mr=Object.defineProperty;var li=Object.getOwnPropertyDescriptor;var di=Object.getOwnPropertyNames;var pi=Object.getPrototypeOf,fi=Object.prototype.hasOwnProperty;var bi=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),gt=(e,t)=>{for(var r in t)mr(e,r,{get:t[r],enumerable:!0})},hi=(e,t,r,u)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of di(t))!fi.call(e,n)&&n!==r&&mr(e,n,{get:()=>t[n],enumerable:!(u=li(t,n))||u.enumerable});return e};var mi=(e,t,r)=>(r=e!=null?si(pi(e)):{},hi(t||!e||!e.__esModule?mr(r,"default",{value:e,enumerable:!0}):r,e));var ea=bi((zf,Jo)=>{function Uo(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(t=>{let r=e[t],u=typeof r;(u==="object"||u==="function")&&!Object.isFrozen(r)&&Uo(r)}),e}var Zt=class{constructor(t){t.data===void 0&&(t.data={}),this.data=t.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}};function Ho(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function Ce(e,...t){let r=Object.create(null);for(let u in e)r[u]=e[u];return t.forEach(function(u){for(let n in u)r[n]=u[n]}),r}var yc="${c}
c?d--:d++,a.__u|=4))):e.__k[o]=null;if(h)for(o=0;o
(p?1:0)){for(n=r-1,o=r+1;n>=0||o =0)m=s.charCodeAt(b.index-1);else for(r=o-1;r>=0&&!(e[r].type==="softbreak"||e[r].type==="hardbreak");r--)if(e[r].content){m=e[r].content.charCodeAt(e[r].content.length-1);break}let x=32;if(c =48&&m<=57&&(f=d=!1),d&&f&&(d=g,f=k),!d&&!f){h&&Ut(n,o,b.index,Qn);continue}if(f)for(r=u.length-1;r>=0;r--){let E=u[r];if(u[r].level=0;t--)e.tokens[t].type!=="inline"||!g0.test(e.tokens[t].content)||_0(e.tokens[t].children,e)}function $r(e){let t,r,u=e.tokens,n=u.length;for(let o=0;o Hello \n world Second Unclosed tags here';
+
+ $result = HtmlToMarkdown::convert($html);
+
+ self::assertIsString($result);
+ self::assertStringContainsString('Unclosed', $result);
+ self::assertStringContainsString('tags', $result);
+ self::assertStringContainsString('here', $result);
+ }
+
+ public function test_deeply_broken_markup_never_fatals(): void
+ {
+ $html = '<<<>>> < # 1 priority 1. First try clearing cache. 1) First step. +1 to that > not a quote Run `npm install` first. 5 * 3 and file_name.txt See [note 1] below. C:\\Users\\me well-known issue Run - unclosed bold, still starts with a dash';
+
+ $result = HtmlToMarkdown::convert($html);
+
+ self::assertStringContainsString('\\-', $result);
+ }
+
+ public function test_toolbar_bold_list_and_link_output_still_convert_normally(): void
+ {
+ self::assertSame('**bold**', HtmlToMarkdown::convert('bold'));
+ self::assertSame("- One\n- Two", HtmlToMarkdown::convert(' It is broken.
+`};fe.fence=function(e,t,r,u,n){let o=e[t],a=o.info?xe(o.info).trim():"",i="",c="";if(a){let p=a.split(/(\s+)/g);i=p[0],c=p.slice(2).join("")}let s;if(r.highlight?s=r.highlight(o.content,i,c)||_e(o.content):s=_e(o.content),s.indexOf(""+_e(e[t].content)+`
+`}return`${s}
`};fe.image=function(e,t,r,u,n){let o=e[t];return o.attrs[o.attrIndex("alt")][1]=n.renderInlineAsText(o.children,r,u),n.renderToken(e,t,r)};fe.hardbreak=function(e,t,r){return r.xhtmlOut?`${s}
`:`
`};fe.softbreak=function(e,t,r){return r.breaks?r.xhtmlOut?`
`:`
`:`
-`};fe.text=function(e,t){return ke(e[t].content)};fe.html_block=function(e,t){return e[t].content};fe.html_inline=function(e,t){return e[t].content};function We(){this.rules=Ve({},fe)}We.prototype.renderAttrs=function(t){let r,u,n;if(!t.attrs)return"";for(n="",r=0,u=t.attrs.length;r
-`:">",o};We.prototype.renderInline=function(e,t,r){let u="",n=this.rules;for(let o=0,a=e.length;o=0&&(u=this.attrs[r][1]),u};Ke.prototype.attrJoin=function(t,r){let u=this.attrIndex(t);u<0?this.attrPush([t,r]):this.attrs[u][1]=this.attrs[u][1]+" "+r};var ye=Ke;function Zn(e,t,r){this.src=e,this.env=r,this.tokens=[],this.inlineMode=!1,this.md=t}Zn.prototype.Token=ye;var Wn=Zn;var i0=/\r\n?|\n/g,s0=/\0/g;function zr(e){let t;t=e.src.replace(i0,`
-`),t=t.replace(s0,"\uFFFD"),e.src=t}function Br(e){let t;e.inlineMode?(t=new e.Token("inline","",0),t.content=e.src,t.map=[0,1],t.children=[],e.tokens.push(t)):e.md.block.parse(e.src,e.md,e.env,e.tokens)}function Pr(e){let t=e.tokens;for(let r=0,u=t.length;r\s]/i.test(e)}function l0(e){return/^<\/a\s*>/i.test(e)}function qr(e){let t=e.tokens;if(e.md.options.linkify)for(let r=0,u=t.length;r=0;a--){let i=n[a];if(i.type==="link_close"){for(a--;n[a].level!==i.level&&n[a].type!=="link_open";)a--;continue}if(i.type==="html_inline"&&(c0(i.content)&&o>0&&o--,l0(i.content)&&o++),!(o>0)&&i.type==="text"&&e.md.linkify.test(i.content)){let s=i.content,c=e.md.linkify.match(s),p=[],b=i.level,d=0;c.length>0&&c[0].index===0&&a>0&&n[a-1].type==="text_special"&&(c=c.slice(1));for(let f=0;f=4||e.src.charCodeAt(n)!==62)return!1;if(u)return!0;let i=[],s=[],c=[],p=[],b=e.md.block.ruler.getRules("blockquote"),d=e.parentType;e.parentType="blockquote";let f=!1,h;for(h=t;hhe((2147483647-n)/c)&&Ce("overflow"),n+=(s-u)*c,u=s;for(let p of e)if(p2147483647&&Ce("overflow"),p===u){let b=n;for(let d=36;;d+=36){let f=d<=o?1:d>=o+26?26:d-o;if(b
After'));
+ }
+
+ public function test_iframe_tag_is_stripped(): void
+ {
+ self::assertSame(
+ 'Before After',
+ HtmlToMarkdown::convert('Before After'),
+ );
+ }
+
+ public function test_entities_are_decoded(): void
+ {
+ self::assertSame('Ben & Jerry\'s "ice cream"', HtmlToMarkdown::convert('Ben & Jerry's "ice cream"'));
+ }
+
+ public function test_inline_elements_keep_the_word_separating_space_between_them(): void
+ {
+ self::assertSame('**Hello** *world*', HtmlToMarkdown::convert('Hello world'));
+ }
+
+ public function test_text_around_an_inline_element_keeps_its_spacing(): void
+ {
+ self::assertSame('foo **bar** baz', HtmlToMarkdown::convert('foo bar baz'));
+ }
+
+ public function test_whitespace_and_blank_lines_are_normalized(): void
+ {
+ $html = "`npm install` first.- item\n# heading\n1. step\n*star*
";
+
+ self::assertSame(
+ "```\n- item\n# heading\n1. step\n*star*\n```",
+ HtmlToMarkdown::convert($html),
+ );
+ }
+
+ public function test_malformed_html_fallback_also_escapes(): void
+ {
+ // Unclosed tag forces the DOM's best-effort path; the leading dash in the
+ // recovered text must still be escaped, not left free to render as a bullet.
+ $html = '
'));
+ self::assertSame(
+ '[Example](https://example.com/page)',
+ HtmlToMarkdown::convert('Example'),
+ );
+ }
+
+ public function test_a_list_items_own_text_starting_with_a_dash_is_still_escaped(): void
+ {
+ $html = '
';
+
+ self::assertSame('- \\- not a nested list, just a dash', HtmlToMarkdown::convert($html));
+ }
+}
diff --git a/tests/TopicControllerTest.php b/tests/TopicControllerTest.php
index 6858ddc..a0f633e 100644
--- a/tests/TopicControllerTest.php
+++ b/tests/TopicControllerTest.php
@@ -64,6 +64,46 @@ public function test_handle_returns_the_created_case_on_success(): void
self::assertSame('pending', $response->get_data()['case']['visibility']);
}
+ public function test_handle_converts_the_body_html_to_markdown_before_calling_the_hub(): void
+ {
+ Functions\when('wp_verify_nonce')->justReturn(1);
+ Functions\when('current_user_can')->justReturn(true);
+
+ $transport = new FakeTransport(200, '{"id":"c1"}');
+ $config = new Config('pk', new FakeConsumer());
+ $controller = new TopicController($config, new HubClient($config, $transport));
+
+ $controller->handle($this->request([
+ 'type' => 'bug',
+ 'title' => 'Broken thing',
+ 'body' => '