markdown.inl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. #pragma once
  2. // License: zlib
  3. // Copyright (c) 2019 Juliette Foucaut & Doug Binks
  4. //
  5. // This software is provided 'as-is', without any express or implied
  6. // warranty. In no event will the authors be held liable for any damages
  7. // arising from the use of this software.
  8. //
  9. // Permission is granted to anyone to use this software for any purpose,
  10. // including commercial applications, and to alter it and redistribute it
  11. // freely, subject to the following restrictions:
  12. //
  13. // 1. The origin of this software must not be misrepresented; you must not
  14. // claim that you wrote the original software. If you use this software
  15. // in a product, an acknowledgment in the product documentation would be
  16. // appreciated but is not required.
  17. // 2. Altered source versions must be plainly marked as such, and must not be
  18. // misrepresented as being the original software.
  19. // 3. This notice may not be removed or altered from any source distribution.
  20. /*
  21. imgui_markdown https://github.com/juliettef/imgui_markdown
  22. Markdown for Dear ImGui
  23. A permissively licensed markdown single-header library for https://github.com/ocornut/imgui
  24. imgui_markdown currently supports the following markdown functionality:
  25. - Wrapped text
  26. - Headers H1, H2, H3
  27. - Indented text, multi levels
  28. - Unordered lists and sub-lists
  29. - Links
  30. Syntax
  31. Wrapping:
  32. Text wraps automatically. To add a new line, use 'Return'.
  33. Headers:
  34. # H1
  35. ## H2
  36. ### H3
  37. Indents:
  38. On a new line, at the start of the line, add two spaces per indent.
  39. ··Indent level 1
  40. ····Indent level 2
  41. Unordered lists:
  42. On a new line, at the start of the line, add two spaces, an asterisks and a space.
  43. For nested lists, add two additional spaces in front of the asterisk per list level increment.
  44. ··*·Unordered List level 1
  45. ····*·Unordered List level 2
  46. Links:
  47. [link description](https://...)
  48. ===============================================================================
  49. // Example use on Windows with links opening in a browser
  50. #include "ImGui.h" // https://github.com/ocornut/imgui
  51. #include "imgui_markdown.h" // https://github.com/juliettef/imgui_markdown
  52. #include "IconsFontAwesome5.h" // https://github.com/juliettef/IconFontCppHeaders
  53. // Following includes for Windows LinkCallback
  54. #define WIN32_LEAN_AND_MEAN
  55. #include <Windows.h>
  56. #include "Shellapi.h"
  57. #include <string>
  58. // You can make your own Markdown function with your prefered string container and markdown config.
  59. static ImGui::MarkdownConfig mdConfig{ LinkCallback, ICON_FA_LINK, { NULL, true, NULL, true, NULL, false } };
  60. void LinkCallback( const char* link_, uint32_t linkLength_ )
  61. {
  62. std::string url( link_, linkLength_ );
  63. ShellExecuteA( NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL );
  64. }
  65. void LoadFonts( float fontSize_ = 12.0f )
  66. {
  67. ImGuiIO& io = ImGui::GetIO();
  68. io.Fonts->Clear();
  69. // Base font
  70. io.Fonts->AddFontFromFileTTF( "myfont.ttf", fontSize_ );
  71. // Bold headings H2 and H3
  72. mdConfig.headingFormats[ 1 ].font = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSize_ );
  73. mdConfig.headingFormats[ 2 ].font = mdConfig.headingFormats[ 1 ].font;
  74. // bold heading H1
  75. float fontSizeH1 = fontSize_ * 1.1f;
  76. mdConfig.headingFormats[ 0 ].font = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSizeH1 );
  77. }
  78. void Markdown( const std::string& markdown_ )
  79. {
  80. // fonts for, respectively, headings H1, H2, H3 and beyond
  81. ImGui::Markdown( markdown_.c_str(), markdown_.length(), mdConfig );
  82. }
  83. void MarkdownExample()
  84. {
  85. const std::string markdownText = u8R"(
  86. # H1 Header: Text and Links
  87. You can add [links like this one to enkisoftware](https://www.enkisoftware.com/) and lines will wrap well.
  88. ## H2 Header: indented text.
  89. This text has an indent (two leading spaces).
  90. This one has two.
  91. ### H3 Header: Lists
  92. * Unordered lists
  93. * Lists can be indented with two extra spaces.
  94. * Lists can have [links like this one to Avoyd](https://www.avoyd.com/)
  95. )";
  96. Markdown( markdownText );
  97. }
  98. ===============================================================================
  99. */
  100. #include <stdint.h>
  101. namespace ImGui
  102. {
  103. // Internals
  104. struct TextRegion;
  105. struct Line;
  106. inline void UnderLine( ImColor col_ );
  107. inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ );
  108. struct TextRegion
  109. {
  110. TextRegion() : indentX( 0.0f )
  111. {
  112. pFont = ImGui::GetFont();
  113. }
  114. ~TextRegion()
  115. {
  116. ResetIndent();
  117. }
  118. // ImGui::TextWrapped will wrap at the starting position
  119. // so to work around this we render using our own wrapping for the first line
  120. void RenderTextWrapped( const char* text, const char* text_end, bool bIndentToHere = false )
  121. {
  122. const float scale = 1.0f;
  123. float widthLeft = GetContentRegionAvail().x;
  124. const char* endPrevLine = pFont->CalcWordWrapPosition( scale, text, text_end, widthLeft );
  125. ImGui::TextUnformatted( text, endPrevLine );
  126. if( bIndentToHere )
  127. {
  128. float indentNeeded = GetContentRegionAvail().x - widthLeft;
  129. if( indentNeeded )
  130. {
  131. ImGui::Indent( indentNeeded );
  132. indentX += indentNeeded;
  133. }
  134. }
  135. widthLeft = GetContentRegionAvail().x;
  136. while( endPrevLine < text_end )
  137. {
  138. text = endPrevLine;
  139. if( *text == ' ' ) { ++text; } // skip a space at start of line
  140. endPrevLine = pFont->CalcWordWrapPosition( scale, text, text_end, widthLeft );
  141. if (text == endPrevLine)
  142. {
  143. endPrevLine++;
  144. }
  145. ImGui::TextUnformatted( text, endPrevLine );
  146. }
  147. }
  148. void RenderListTextWrapped( const char* text, const char* text_end )
  149. {
  150. ImGui::Bullet();
  151. ImGui::SameLine();
  152. RenderTextWrapped( text, text_end, true );
  153. }
  154. void ResetIndent()
  155. {
  156. if( indentX > 0.0f )
  157. {
  158. ImGui::Unindent( indentX );
  159. }
  160. indentX = 0.0f;
  161. }
  162. private:
  163. float indentX;
  164. ImFont* pFont;
  165. };
  166. // Text that starts after a new line (or at beginning) and ends with a newline (or at end)
  167. struct Line {
  168. bool isHeading = false;
  169. bool isUnorderedListStart = false;
  170. bool isLeadingSpace = true; // spaces at start of line
  171. int leadSpaceCount = 0;
  172. int headingCount = 0;
  173. int lineStart = 0;
  174. int lineEnd = 0;
  175. int lastRenderPosition = 0; // lines may get rendered in multiple pieces
  176. };
  177. struct TextBlock { // subset of line
  178. int start = 0;
  179. int stop = 0;
  180. int size() const
  181. {
  182. return stop - start;
  183. }
  184. };
  185. struct Link {
  186. enum LinkState {
  187. NO_LINK,
  188. HAS_SQUARE_BRACKET_OPEN,
  189. HAS_SQUARE_BRACKETS,
  190. HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN,
  191. };
  192. LinkState state = NO_LINK;
  193. TextBlock text;
  194. TextBlock url;
  195. };
  196. inline void UnderLine( ImColor col_ )
  197. {
  198. ImVec2 min = ImGui::GetItemRectMin();
  199. ImVec2 max = ImGui::GetItemRectMax();
  200. min.y = max.y;
  201. ImGui::GetWindowDrawList()->AddLine( min, max, col_, 1.0f );
  202. }
  203. inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ )
  204. {
  205. // indent
  206. int indentStart = 0;
  207. if( line_.isUnorderedListStart ) // ImGui unordered list render always adds one indent
  208. {
  209. indentStart = 1;
  210. }
  211. for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j ) // add indents
  212. {
  213. ImGui::Indent();
  214. }
  215. // render
  216. int textStart = line_.lastRenderPosition + 1;
  217. int textSize = line_.lineEnd - textStart;
  218. if( line_.isUnorderedListStart ) // render unordered list
  219. {
  220. const char* text = markdown_ + textStart + 1;
  221. textRegion_.RenderListTextWrapped( text, text + textSize - 1 );
  222. }
  223. else if( line_.isHeading ) // render heading
  224. {
  225. MarkdownConfig::HeadingFormat fmt;
  226. if( line_.headingCount > mdConfig_.NUMHEADINGS )
  227. {
  228. fmt = mdConfig_.headingFormats[ mdConfig_.NUMHEADINGS - 1 ];
  229. }
  230. else
  231. {
  232. fmt = mdConfig_.headingFormats[ line_.headingCount - 1 ];
  233. }
  234. bool popFontRequired = false;
  235. if( fmt.font && fmt.font != ImGui::GetFont() )
  236. {
  237. ImGui::PushFont( fmt.font, 0.0f );
  238. popFontRequired = true;
  239. }
  240. const char* text = markdown_ + textStart + 1;
  241. ImGui::NewLine();
  242. textRegion_.RenderTextWrapped( text, text + textSize - 1 );
  243. if( fmt.separator )
  244. {
  245. ImGui::Separator();
  246. }
  247. ImGui::NewLine();
  248. if( popFontRequired )
  249. {
  250. ImGui::PopFont();
  251. }
  252. }
  253. else // render a normal paragraph chunk
  254. {
  255. const char* text = markdown_ + textStart;
  256. textRegion_.RenderTextWrapped( text, text + textSize );
  257. }
  258. // unindent
  259. for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j )
  260. {
  261. ImGui::Unindent();
  262. }
  263. }
  264. // render markdown
  265. void Markdown( const char* markdown_, int32_t markdownLength_, const MarkdownConfig& mdConfig_ )
  266. {
  267. ImGuiStyle& style = ImGui::GetStyle();
  268. Line line;
  269. Link link;
  270. TextRegion textRegion;
  271. char c = 0;
  272. for( int i=0; i < markdownLength_; ++i )
  273. {
  274. c = markdown_[i]; // get the character at index
  275. if( c == 0 ) { break; } // shouldn't happen but don't go beyond 0.
  276. // If we're at the beginning of the line, count any spaces
  277. if( line.isLeadingSpace )
  278. {
  279. if( c == ' ' )
  280. {
  281. ++line.leadSpaceCount;
  282. continue;
  283. }
  284. else
  285. {
  286. line.isLeadingSpace = false;
  287. line.lastRenderPosition = i - 1;
  288. if(( c == '*' ) && ( line.leadSpaceCount >= 2 ))
  289. {
  290. if(( markdownLength_ > i + 1 ) && ( markdown_[ i + 1 ] == ' ' )) // space after '*'
  291. {
  292. line.isUnorderedListStart = true;
  293. ++i;
  294. ++line.lastRenderPosition;
  295. }
  296. continue;
  297. }
  298. else if( c == '#' )
  299. {
  300. line.headingCount++;
  301. bool bContinueChecking = true;
  302. int32_t j = i;
  303. while( ++j < markdownLength_ && bContinueChecking )
  304. {
  305. c = markdown_[j];
  306. switch( c )
  307. {
  308. case '#':
  309. line.headingCount++;
  310. break;
  311. case ' ':
  312. line.lastRenderPosition = j - 1;
  313. i = j;
  314. line.isHeading = true;
  315. bContinueChecking = false;
  316. break;
  317. default:
  318. line.isHeading = false;
  319. bContinueChecking = false;
  320. break;
  321. }
  322. }
  323. if( line.isHeading ) { continue; }
  324. }
  325. }
  326. }
  327. // Test to see if we have a link
  328. switch( link.state )
  329. {
  330. case Link::NO_LINK:
  331. if( c == '[' )
  332. {
  333. link.state = Link::HAS_SQUARE_BRACKET_OPEN;
  334. link.text.start = i + 1;
  335. }
  336. break;
  337. case Link::HAS_SQUARE_BRACKET_OPEN:
  338. if( c == ']' )
  339. {
  340. link.state = Link::HAS_SQUARE_BRACKETS;
  341. link.text.stop = i;
  342. }
  343. break;
  344. case Link::HAS_SQUARE_BRACKETS:
  345. if( c == '(' )
  346. {
  347. link.state = Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN;
  348. link.url.start = i + 1;
  349. }
  350. break;
  351. case Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN:
  352. if( c == ')' ) // it's a link, render it.
  353. {
  354. // render previous line content
  355. line.lineEnd = link.text.start - 1;
  356. RenderLine( markdown_, line, textRegion, mdConfig_ );
  357. line.leadSpaceCount = 0;
  358. line.isUnorderedListStart = false; // the following text shouldn't have bullets
  359. // render link
  360. link.url.stop = i;
  361. ImGui::SameLine( 0.0f, 0.0f );
  362. ImGui::PushStyleColor( ImGuiCol_Text, style.Colors[ ImGuiCol_ButtonHovered ]);
  363. ImGui::PushTextWrapPos(-1.0f);
  364. const char* text = markdown_ + link.text.start ;
  365. ImGui::TextUnformatted( text, text + link.text.size() );
  366. ImGui::PopTextWrapPos();
  367. ImGui::PopStyleColor();
  368. if (ImGui::IsItemHovered())
  369. {
  370. if( ImGui::IsMouseClicked(0) )
  371. {
  372. if( mdConfig_.linkCallback )
  373. {
  374. mdConfig_.linkCallback( markdown_ + link.url.start, link.url.size() );
  375. }
  376. }
  377. ImGui::UnderLine( style.Colors[ ImGuiCol_ButtonHovered ] );
  378. ImGui::SetTooltip( "%s Open in browser\n%.*s", mdConfig_.linkIcon, link.url.size(), markdown_ + link.url.start );
  379. }
  380. else
  381. {
  382. ImGui::UnderLine( style.Colors[ ImGuiCol_Button ] );
  383. }
  384. ImGui::SameLine( 0.0f, 0.0f );
  385. // reset the link by reinitializing it
  386. link = Link();
  387. line.lastRenderPosition = i;
  388. }
  389. break;
  390. }
  391. // handle end of line (render)
  392. if( c == '\n' )
  393. {
  394. // render the line
  395. line.lineEnd = i;
  396. RenderLine( markdown_, line, textRegion, mdConfig_ );
  397. // reset the line
  398. line = Line();
  399. line.lineStart = i + 1;
  400. line.lastRenderPosition = i;
  401. textRegion.ResetIndent();
  402. // reset the link
  403. link = Link();
  404. }
  405. }
  406. // render any remaining text if last char wasn't 0
  407. if( markdownLength_ && line.lineStart < (int)markdownLength_ && markdown_[ line.lineStart ] != 0 )
  408. {
  409. line.lineEnd = (int)markdownLength_ - 1;
  410. RenderLine( markdown_, line, textRegion, mdConfig_ );
  411. }
  412. }
  413. }