dataface 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. mdsvg/utils.py +86 -0
mdsvg/parser.py ADDED
@@ -0,0 +1,629 @@
1
+ """Markdown parser that produces an AST."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import List, Optional, Tuple
7
+
8
+ from .types import (
9
+ AnyBlock,
10
+ Blockquote,
11
+ CodeBlock,
12
+ Document,
13
+ Heading,
14
+ HorizontalRule,
15
+ ImageBlock,
16
+ ListItem,
17
+ OrderedList,
18
+ Paragraph,
19
+ RawHtmlBlock,
20
+ Span,
21
+ SpanType,
22
+ Table,
23
+ TableCell,
24
+ TableRow,
25
+ UnorderedList,
26
+ )
27
+ from .utils import normalize_whitespace, split_lines
28
+
29
+
30
+ class MarkdownParser:
31
+ """
32
+ Parser that converts Markdown text to an AST.
33
+
34
+ The parser handles common Markdown syntax including:
35
+ - Headings (ATX style: # through ######)
36
+ - Paragraphs with inline formatting
37
+ - Bold, italic, inline code, links
38
+ - Unordered and ordered lists
39
+ - Code blocks (fenced and indented)
40
+ - Blockquotes
41
+ - Horizontal rules
42
+ - Tables (GFM style)
43
+ - Images
44
+
45
+ Example:
46
+ >>> parser = MarkdownParser()
47
+ >>> doc = parser.parse("# Hello\\n\\nThis is **bold**.")
48
+ >>> print(doc[0]) # Heading
49
+ >>> print(doc[1]) # Paragraph
50
+ """
51
+
52
+ # Regex patterns for block-level elements
53
+ HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.+)$")
54
+ FENCED_CODE_START = re.compile(r"^```(\w*)\s*$")
55
+ FENCED_CODE_END = re.compile(r"^```\s*$")
56
+ HORIZONTAL_RULE = re.compile(r"^(?:[-*_]){3,}\s*$")
57
+ UNORDERED_LIST_ITEM = re.compile(r"^(\s*)([-*+])\s+(.+)$")
58
+ ORDERED_LIST_ITEM = re.compile(r"^(\s*)(\d+)\.\s+(.+)$")
59
+ BLOCKQUOTE = re.compile(r"^>\s?(.*)$")
60
+ TABLE_ROW = re.compile(r"^\|(.+)\|$")
61
+ TABLE_SEPARATOR = re.compile(r"^\|[\s\-:|]+\|$")
62
+ # Image block with optional {width=X height=Y} attributes
63
+ IMAGE_BLOCK = re.compile(
64
+ r"^!\[([^\]]*)\]\(([^)\s]+)(?:\s+[\"']([^\"']+)[\"'])?\)"
65
+ r"(?:\{([^}]+)\})?\s*$"
66
+ )
67
+ # HTML block: line starting with < followed by a tag name
68
+ HTML_BLOCK_START = re.compile(r"^<([a-zA-Z][a-zA-Z0-9]*)\b")
69
+ # Self-closing or void HTML tags that are always single-line
70
+ HTML_VOID_TAGS = frozenset(
71
+ {
72
+ "area",
73
+ "base",
74
+ "br",
75
+ "col",
76
+ "embed",
77
+ "hr",
78
+ "img",
79
+ "input",
80
+ "link",
81
+ "meta",
82
+ "param",
83
+ "source",
84
+ "track",
85
+ "wbr",
86
+ }
87
+ )
88
+ # Pattern to extract key=value pairs from image attributes
89
+ IMAGE_ATTR = re.compile(r"(\w+)\s*=\s*(\d+(?:\.\d+)?)")
90
+
91
+ # Regex patterns for inline elements
92
+ INLINE_CODE = re.compile(r"`([^`]+)`")
93
+ BOLD_ITALIC = re.compile(r"\*\*\*(.+?)\*\*\*|___(.+?)___")
94
+ BOLD = re.compile(r"\*\*(.+?)\*\*|__(.+?)__")
95
+ ITALIC = re.compile(r"\*([^*]+)\*|_([^_]+)_")
96
+ LINK = re.compile(r"\[([^\]]+)\]\(([^)\s]+)(?:\s+[\"']([^\"']+)[\"'])?\)")
97
+ IMAGE = re.compile(r"!\[([^\]]*)\]\(([^)\s]+)(?:\s+[\"']([^\"']+)[\"'])?\)")
98
+
99
+ def parse(self, text: str) -> Document:
100
+ """
101
+ Parse Markdown text into a document AST.
102
+
103
+ Args:
104
+ text: Markdown text to parse.
105
+
106
+ Returns:
107
+ List of Block objects representing the document.
108
+ """
109
+ if not text or not text.strip():
110
+ return []
111
+
112
+ lines = split_lines(text)
113
+ return self._parse_blocks(lines)
114
+
115
+ def _parse_blocks(self, lines: List[str]) -> Document:
116
+ """Parse a list of lines into blocks."""
117
+ blocks: Document = []
118
+ i = 0
119
+
120
+ while i < len(lines):
121
+ line = lines[i]
122
+
123
+ # Skip empty lines
124
+ if not line.strip():
125
+ i += 1
126
+ continue
127
+
128
+ # Try each block type
129
+ block, consumed = self._try_parse_block(lines, i)
130
+
131
+ if block is not None:
132
+ blocks.append(block)
133
+ i += consumed
134
+ else:
135
+ # Default to paragraph - collect until empty line or other block
136
+ para_lines, consumed = self._collect_paragraph_lines(lines, i)
137
+ if para_lines:
138
+ para_text = " ".join(para_lines)
139
+ spans = self._parse_inline(para_text)
140
+ blocks.append(Paragraph(spans=tuple(spans)))
141
+ i += consumed
142
+
143
+ return blocks
144
+
145
+ def _try_parse_block(
146
+ self, lines: List[str], start: int
147
+ ) -> Tuple[Optional[AnyBlock], int]:
148
+ """Try to parse a block starting at the given line index."""
149
+ line = lines[start]
150
+
151
+ # Heading
152
+ match = self.HEADING_PATTERN.match(line)
153
+ if match:
154
+ level = len(match.group(1))
155
+ text = match.group(2).strip()
156
+ spans = self._parse_inline(text)
157
+ return Heading(level=level, spans=tuple(spans)), 1
158
+
159
+ # Horizontal rule
160
+ if self.HORIZONTAL_RULE.match(line):
161
+ return HorizontalRule(), 1
162
+
163
+ # Fenced code block
164
+ match = self.FENCED_CODE_START.match(line)
165
+ if match:
166
+ language = match.group(1) or None
167
+ code_lines: List[str] = []
168
+ i = start + 1
169
+ while i < len(lines):
170
+ if self.FENCED_CODE_END.match(lines[i]):
171
+ i += 1
172
+ break
173
+ code_lines.append(lines[i])
174
+ i += 1
175
+ code = "\n".join(code_lines)
176
+ return CodeBlock(code=code, language=language), i - start
177
+
178
+ # Indented code block (4 spaces or 1 tab)
179
+ if line.startswith(" ") or line.startswith("\t"):
180
+ code_lines_indented: List[str] = []
181
+ i = start
182
+ while i < len(lines):
183
+ current = lines[i]
184
+ if current.startswith(" "):
185
+ code_lines_indented.append(current[4:])
186
+ elif current.startswith("\t"):
187
+ code_lines_indented.append(current[1:])
188
+ elif not current.strip():
189
+ code_lines_indented.append("")
190
+ else:
191
+ break
192
+ i += 1
193
+ # Remove trailing empty lines
194
+ while code_lines_indented and not code_lines_indented[-1]:
195
+ code_lines_indented.pop()
196
+ if code_lines_indented:
197
+ return CodeBlock(code="\n".join(code_lines_indented)), i - start
198
+
199
+ # Blockquote
200
+ match = self.BLOCKQUOTE.match(line)
201
+ if match:
202
+ quote_lines: List[str] = []
203
+ i = start
204
+ while i < len(lines):
205
+ bq_match = self.BLOCKQUOTE.match(lines[i])
206
+ if bq_match:
207
+ quote_lines.append(bq_match.group(1))
208
+ elif not lines[i].strip():
209
+ # Empty line might continue quote
210
+ if i + 1 < len(lines) and self.BLOCKQUOTE.match(lines[i + 1]):
211
+ quote_lines.append("")
212
+ else:
213
+ break
214
+ else:
215
+ break
216
+ i += 1
217
+ # Parse the content inside the blockquote
218
+ inner_text = "\n".join(quote_lines)
219
+ inner_blocks = self.parse(inner_text)
220
+ return Blockquote(blocks=tuple(inner_blocks)), i - start
221
+
222
+ # Table
223
+ if self.TABLE_ROW.match(line):
224
+ table, consumed = self._parse_table(lines, start)
225
+ if table:
226
+ return table, consumed
227
+
228
+ # Unordered list
229
+ match = self.UNORDERED_LIST_ITEM.match(line)
230
+ if match:
231
+ return self._parse_unordered_list(lines, start)
232
+
233
+ # Ordered list
234
+ match = self.ORDERED_LIST_ITEM.match(line)
235
+ if match:
236
+ return self._parse_ordered_list(lines, start)
237
+
238
+ # Image block (standalone)
239
+ match = self.IMAGE_BLOCK.match(line.strip())
240
+ if match:
241
+ alt = match.group(1)
242
+ url = match.group(2)
243
+ title = match.group(3) if match.group(3) else None
244
+
245
+ # Parse optional {width=X height=Y} attributes
246
+ width: Optional[float] = None
247
+ height: Optional[float] = None
248
+ attrs_str = match.group(4)
249
+ if attrs_str:
250
+ for attr_match in self.IMAGE_ATTR.finditer(attrs_str):
251
+ key = attr_match.group(1).lower()
252
+ value = float(attr_match.group(2))
253
+ if key == "width":
254
+ width = value
255
+ elif key == "height":
256
+ height = value
257
+
258
+ return (
259
+ ImageBlock(url=url, alt=alt, title=title, width=width, height=height),
260
+ 1,
261
+ )
262
+
263
+ # HTML block
264
+ html_match = self.HTML_BLOCK_START.match(line)
265
+ if html_match:
266
+ return self._parse_html_block(lines, start, html_match.group(1))
267
+
268
+ return None, 0
269
+
270
+ def _collect_paragraph_lines(
271
+ self, lines: List[str], start: int
272
+ ) -> Tuple[List[str], int]:
273
+ """Collect lines that belong to a paragraph."""
274
+ para_lines: List[str] = []
275
+ i = start
276
+
277
+ while i < len(lines):
278
+ line = lines[i]
279
+
280
+ # Empty line ends paragraph
281
+ if not line.strip():
282
+ break
283
+
284
+ # Check if line starts a new block type
285
+ if (
286
+ self.HEADING_PATTERN.match(line)
287
+ or self.HORIZONTAL_RULE.match(line)
288
+ or self.FENCED_CODE_START.match(line)
289
+ or self.BLOCKQUOTE.match(line)
290
+ or self.UNORDERED_LIST_ITEM.match(line)
291
+ or self.ORDERED_LIST_ITEM.match(line)
292
+ or self.TABLE_ROW.match(line)
293
+ or (line.startswith(" ") and not para_lines)
294
+ or self.HTML_BLOCK_START.match(line)
295
+ ):
296
+ break
297
+
298
+ para_lines.append(normalize_whitespace(line))
299
+ i += 1
300
+
301
+ return para_lines, i - start
302
+
303
+ def _parse_html_block(
304
+ self, lines: List[str], start: int, tag_name: str
305
+ ) -> Tuple[RawHtmlBlock, int]:
306
+ """Parse an HTML block starting at the given line.
307
+
308
+ Collects lines until the closing tag is found (for paired tags)
309
+ or returns a single line (for self-closing / void tags).
310
+ """
311
+ tag_lower = tag_name.lower()
312
+ first_line = lines[start]
313
+
314
+ # Void/self-closing tags or single-line with closing tag
315
+ if tag_lower in self.HTML_VOID_TAGS or f"</{tag_name}>" in first_line:
316
+ return RawHtmlBlock(html=first_line), 1
317
+
318
+ # Multi-line: collect until closing tag
319
+ closing = f"</{tag_name}>"
320
+ closing_lower = closing.lower()
321
+ html_lines: List[str] = [first_line]
322
+ i = start + 1
323
+ while i < len(lines):
324
+ html_lines.append(lines[i])
325
+ if closing_lower in lines[i].lower():
326
+ i += 1
327
+ break
328
+ i += 1
329
+
330
+ return RawHtmlBlock(html="\n".join(html_lines)), i - start
331
+
332
+ def _parse_unordered_list(
333
+ self, lines: List[str], start: int
334
+ ) -> Tuple[UnorderedList, int]:
335
+ """Parse an unordered list."""
336
+ items: List[ListItem] = []
337
+ i = start
338
+ base_indent: Optional[int] = None
339
+
340
+ while i < len(lines):
341
+ line = lines[i]
342
+
343
+ if not line.strip():
344
+ i += 1
345
+ continue
346
+
347
+ match = self.UNORDERED_LIST_ITEM.match(line)
348
+ if match:
349
+ indent = len(match.group(1))
350
+
351
+ if base_indent is None:
352
+ base_indent = indent
353
+ elif indent < base_indent:
354
+ break
355
+ elif indent > base_indent:
356
+ # Nested list - for now, just treat as continuation
357
+ pass
358
+
359
+ if indent == base_indent:
360
+ text = match.group(3)
361
+ spans = self._parse_inline(text)
362
+ items.append(ListItem(spans=tuple(spans)))
363
+ i += 1
364
+ else:
365
+ # Check if it's a continuation or a different block
366
+ if not line.startswith(" ") and not line.startswith("\t"):
367
+ break
368
+ i += 1
369
+
370
+ return UnorderedList(items=tuple(items)), i - start
371
+
372
+ def _parse_ordered_list(
373
+ self, lines: List[str], start: int
374
+ ) -> Tuple[OrderedList, int]:
375
+ """Parse an ordered list."""
376
+ items: List[ListItem] = []
377
+ i = start
378
+ base_indent: Optional[int] = None
379
+ start_num = 1
380
+
381
+ while i < len(lines):
382
+ line = lines[i]
383
+
384
+ if not line.strip():
385
+ i += 1
386
+ continue
387
+
388
+ match = self.ORDERED_LIST_ITEM.match(line)
389
+ if match:
390
+ indent = len(match.group(1))
391
+
392
+ if base_indent is None:
393
+ base_indent = indent
394
+ start_num = int(match.group(2))
395
+ elif indent < base_indent:
396
+ break
397
+
398
+ if indent == base_indent:
399
+ text = match.group(3)
400
+ spans = self._parse_inline(text)
401
+ items.append(ListItem(spans=tuple(spans)))
402
+ i += 1
403
+ else:
404
+ if not line.startswith(" ") and not line.startswith("\t"):
405
+ break
406
+ i += 1
407
+
408
+ return OrderedList(items=tuple(items), start=start_num), i - start
409
+
410
+ def _parse_table(self, lines: List[str], start: int) -> Tuple[Optional[Table], int]:
411
+ """Parse a GFM-style table."""
412
+ if start + 1 >= len(lines):
413
+ return None, 0
414
+
415
+ header_line = lines[start]
416
+ separator_line = lines[start + 1]
417
+
418
+ # Must have header row and separator row
419
+ if not self.TABLE_ROW.match(header_line):
420
+ return None, 0
421
+ if not self.TABLE_SEPARATOR.match(separator_line):
422
+ return None, 0
423
+
424
+ # Parse alignments from separator
425
+ alignments = self._parse_table_alignments(separator_line)
426
+
427
+ # Parse header
428
+ header_cells = self._parse_table_row(header_line, alignments)
429
+ header = TableRow(cells=tuple(header_cells))
430
+
431
+ # Parse body rows
432
+ rows: List[TableRow] = []
433
+ i = start + 2
434
+
435
+ while i < len(lines):
436
+ line = lines[i]
437
+ if not self.TABLE_ROW.match(line):
438
+ break
439
+
440
+ row_cells = self._parse_table_row(line, alignments)
441
+ rows.append(TableRow(cells=tuple(row_cells)))
442
+ i += 1
443
+
444
+ return (
445
+ Table(
446
+ header=header,
447
+ rows=tuple(rows),
448
+ ),
449
+ i - start,
450
+ )
451
+
452
+ def _parse_table_alignments(self, separator: str) -> List[Optional[str]]:
453
+ """Parse column alignments from table separator row."""
454
+ alignments: List[Optional[str]] = []
455
+
456
+ # Remove outer pipes and split
457
+ content = separator.strip().strip("|")
458
+ cells = content.split("|")
459
+
460
+ for cell in cells:
461
+ cell = cell.strip()
462
+ if cell.startswith(":") and cell.endswith(":"):
463
+ alignments.append("center")
464
+ elif cell.endswith(":"):
465
+ alignments.append("right")
466
+ elif cell.startswith(":"):
467
+ alignments.append("left")
468
+ else:
469
+ alignments.append(None)
470
+
471
+ return alignments
472
+
473
+ def _parse_table_row(
474
+ self,
475
+ line: str,
476
+ alignments: List[Optional[str]],
477
+ ) -> List[TableCell]:
478
+ """Parse a single table row."""
479
+ # Remove outer pipes and split
480
+ content = line.strip().strip("|")
481
+ cell_texts = content.split("|")
482
+
483
+ cells: List[TableCell] = []
484
+ for idx, cell_text in enumerate(cell_texts):
485
+ cell_text = cell_text.strip()
486
+ spans = self._parse_inline(cell_text)
487
+ align = alignments[idx] if idx < len(alignments) else None
488
+ cells.append(TableCell(spans=tuple(spans), align=align))
489
+
490
+ return cells
491
+
492
+ def _parse_inline(self, text: str) -> List[Span]:
493
+ """
494
+ Parse inline formatting in text.
495
+
496
+ Handles: bold, italic, bold+italic, inline code, links, images.
497
+ """
498
+ if not text:
499
+ return []
500
+
501
+ spans: List[Span] = []
502
+ remaining = text
503
+
504
+ while remaining:
505
+ # Find the earliest match
506
+ earliest_match = None
507
+ earliest_pos = len(remaining)
508
+ match_type = None
509
+
510
+ # Check for inline code first (highest priority)
511
+ match = self.INLINE_CODE.search(remaining)
512
+ if match and match.start() < earliest_pos:
513
+ earliest_match = match
514
+ earliest_pos = match.start()
515
+ match_type = "code"
516
+
517
+ # Check for images (before links, since syntax overlaps)
518
+ match = self.IMAGE.search(remaining)
519
+ if match and match.start() < earliest_pos:
520
+ earliest_match = match
521
+ earliest_pos = match.start()
522
+ match_type = "image"
523
+
524
+ # Check for links
525
+ match = self.LINK.search(remaining)
526
+ if match and match.start() < earliest_pos:
527
+ earliest_match = match
528
+ earliest_pos = match.start()
529
+ match_type = "link"
530
+
531
+ # Check for bold+italic
532
+ match = self.BOLD_ITALIC.search(remaining)
533
+ if match and match.start() < earliest_pos:
534
+ earliest_match = match
535
+ earliest_pos = match.start()
536
+ match_type = "bold_italic"
537
+
538
+ # Check for bold
539
+ match = self.BOLD.search(remaining)
540
+ if match and match.start() < earliest_pos:
541
+ earliest_match = match
542
+ earliest_pos = match.start()
543
+ match_type = "bold"
544
+
545
+ # Check for italic
546
+ match = self.ITALIC.search(remaining)
547
+ if match and match.start() < earliest_pos:
548
+ earliest_match = match
549
+ earliest_pos = match.start()
550
+ match_type = "italic"
551
+
552
+ if earliest_match is None:
553
+ # No more formatting, add remaining as text
554
+ if remaining:
555
+ spans.append(Span(text=remaining))
556
+ break
557
+
558
+ # Add text before the match
559
+ if earliest_pos > 0:
560
+ spans.append(Span(text=remaining[:earliest_pos]))
561
+
562
+ # Process the match
563
+ if match_type == "code":
564
+ code_text = earliest_match.group(1)
565
+ spans.append(Span(text=code_text, span_type=SpanType.CODE))
566
+
567
+ elif match_type == "image":
568
+ alt = earliest_match.group(1)
569
+ url = earliest_match.group(2)
570
+ title = (
571
+ earliest_match.group(3)
572
+ if earliest_match.lastindex and earliest_match.lastindex >= 3
573
+ else None
574
+ )
575
+ spans.append(
576
+ Span(
577
+ text=alt or url, span_type=SpanType.IMAGE, url=url, title=title
578
+ )
579
+ )
580
+
581
+ elif match_type == "link":
582
+ link_text = earliest_match.group(1)
583
+ url = earliest_match.group(2)
584
+ title = (
585
+ earliest_match.group(3)
586
+ if earliest_match.lastindex and earliest_match.lastindex >= 3
587
+ else None
588
+ )
589
+ spans.append(
590
+ Span(text=link_text, span_type=SpanType.LINK, url=url, title=title)
591
+ )
592
+
593
+ elif match_type == "bold_italic":
594
+ inner = earliest_match.group(1) or earliest_match.group(2)
595
+ spans.append(Span(text=inner, span_type=SpanType.BOLD_ITALIC))
596
+
597
+ elif match_type == "bold":
598
+ inner = earliest_match.group(1) or earliest_match.group(2)
599
+ spans.append(Span(text=inner, span_type=SpanType.BOLD))
600
+
601
+ elif match_type == "italic":
602
+ inner = earliest_match.group(1) or earliest_match.group(2)
603
+ spans.append(Span(text=inner, span_type=SpanType.ITALIC))
604
+
605
+ # Continue with rest of text
606
+ remaining = remaining[earliest_match.end() :]
607
+
608
+ return spans
609
+
610
+
611
+ # Convenience function
612
+ def parse(text: str) -> Document:
613
+ """
614
+ Parse Markdown text into a document AST.
615
+
616
+ This is a convenience function that creates a MarkdownParser
617
+ and calls parse() on it.
618
+
619
+ Args:
620
+ text: Markdown text to parse.
621
+
622
+ Returns:
623
+ List of Block objects representing the document.
624
+
625
+ Example:
626
+ >>> doc = parse("# Hello World")
627
+ >>> print(doc[0].level) # 1
628
+ """
629
+ return MarkdownParser().parse(text)