@dxos/plugin-magazine 0.0.0

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 (390) hide show
  1. package/LICENSE +105 -0
  2. package/PLUGIN.mdl +653 -0
  3. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
  4. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
  5. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
  6. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
  7. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
  8. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
  9. package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
  10. package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
  11. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
  12. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
  13. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
  14. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
  15. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
  16. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
  17. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
  18. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  20. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
  22. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
  24. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
  25. package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
  26. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
  27. package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
  28. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
  32. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
  33. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
  34. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
  35. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
  36. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
  37. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
  38. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
  39. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
  40. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
  41. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
  42. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
  43. package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
  44. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
  45. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
  46. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
  47. package/dist/lib/neutral/components/index.mjs +338 -0
  48. package/dist/lib/neutral/components/index.mjs.map +7 -0
  49. package/dist/lib/neutral/containers/index.mjs +17 -0
  50. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  51. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
  52. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
  53. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
  54. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
  55. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
  56. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
  57. package/dist/lib/neutral/index.mjs +18 -0
  58. package/dist/lib/neutral/index.mjs.map +7 -0
  59. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
  60. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
  61. package/dist/lib/neutral/meta.json +1 -0
  62. package/dist/lib/neutral/meta.mjs +8 -0
  63. package/dist/lib/neutral/meta.mjs.map +7 -0
  64. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
  65. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
  66. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
  67. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
  68. package/dist/lib/neutral/operations/index.mjs +8 -0
  69. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  70. package/dist/lib/neutral/plugin.mjs +16 -0
  71. package/dist/lib/neutral/plugin.mjs.map +7 -0
  72. package/dist/lib/neutral/plugin.workerd.mjs +16 -0
  73. package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
  74. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
  75. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
  76. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
  77. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
  78. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
  79. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
  80. package/dist/lib/neutral/skills/index.mjs +46 -0
  81. package/dist/lib/neutral/skills/index.mjs.map +7 -0
  82. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
  83. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
  84. package/dist/lib/neutral/translations.mjs +81 -0
  85. package/dist/lib/neutral/translations.mjs.map +7 -0
  86. package/dist/lib/neutral/types/index.mjs +14 -0
  87. package/dist/lib/neutral/types/index.mjs.map +7 -0
  88. package/dist/types/dx.config.d.ts +28 -0
  89. package/dist/types/dx.config.d.ts.map +1 -0
  90. package/dist/types/src/MagazinePlugin.d.ts +4 -0
  91. package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
  92. package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
  93. package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
  94. package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
  95. package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
  96. package/dist/types/src/atoms/atoms.test.d.ts +2 -0
  97. package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
  98. package/dist/types/src/atoms/index.d.ts +7 -0
  99. package/dist/types/src/atoms/index.d.ts.map +1 -0
  100. package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
  101. package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
  102. package/dist/types/src/atoms/post-content.d.ts +7 -0
  103. package/dist/types/src/atoms/post-content.d.ts.map +1 -0
  104. package/dist/types/src/atoms/post-curation.d.ts +16 -0
  105. package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
  106. package/dist/types/src/atoms/post-display.d.ts +22 -0
  107. package/dist/types/src/atoms/post-display.d.ts.map +1 -0
  108. package/dist/types/src/atoms/post-read.d.ts +14 -0
  109. package/dist/types/src/atoms/post-read.d.ts.map +1 -0
  110. package/dist/types/src/atoms/post-tags.d.ts +16 -0
  111. package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
  113. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/create-object.d.ts +5 -0
  115. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/index.d.ts +10 -0
  117. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
  119. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  121. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  123. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
  125. package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
  127. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
  128. package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
  129. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
  130. package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
  131. package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
  132. package/dist/types/src/components/PostContent/index.d.ts +2 -0
  133. package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
  134. package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
  135. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
  136. package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
  137. package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
  138. package/dist/types/src/components/PostStack/index.d.ts +2 -0
  139. package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
  140. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
  141. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
  142. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
  143. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
  144. package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
  145. package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
  146. package/dist/types/src/components/index.d.ts +4 -0
  147. package/dist/types/src/components/index.d.ts.map +1 -0
  148. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
  149. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
  150. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
  151. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
  152. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
  153. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
  154. package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
  155. package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
  156. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
  157. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
  158. package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
  159. package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
  160. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
  161. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
  162. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
  163. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
  164. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
  165. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
  166. package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
  167. package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
  168. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
  169. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
  170. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
  171. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
  172. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
  173. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
  174. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
  175. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
  176. package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
  177. package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
  178. package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
  179. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
  180. package/dist/types/src/containers/PostCard/index.d.ts +2 -0
  181. package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
  182. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
  183. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
  184. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
  185. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
  186. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
  187. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
  188. package/dist/types/src/containers/index.d.ts +7 -0
  189. package/dist/types/src/containers/index.d.ts.map +1 -0
  190. package/dist/types/src/index.d.ts +3 -0
  191. package/dist/types/src/index.d.ts.map +1 -0
  192. package/dist/types/src/meta.d.ts +33 -0
  193. package/dist/types/src/meta.d.ts.map +1 -0
  194. package/dist/types/src/operations/clear-magazine.d.ts +10 -0
  195. package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
  196. package/dist/types/src/operations/curate-magazine.d.ts +25 -0
  197. package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
  198. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
  199. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
  200. package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
  201. package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
  202. package/dist/types/src/operations/extraction/article.d.ts +37 -0
  203. package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
  204. package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
  205. package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
  206. package/dist/types/src/operations/extraction/index.d.ts +2 -0
  207. package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
  208. package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
  209. package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
  210. package/dist/types/src/operations/index.d.ts +3 -0
  211. package/dist/types/src/operations/index.d.ts.map +1 -0
  212. package/dist/types/src/operations/load-post-content.d.ts +8 -0
  213. package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
  214. package/dist/types/src/operations/sources/article.d.ts +24 -0
  215. package/dist/types/src/operations/sources/article.d.ts.map +1 -0
  216. package/dist/types/src/operations/sources/cors.d.ts +12 -0
  217. package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
  218. package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
  219. package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
  220. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
  221. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
  222. package/dist/types/src/operations/sources/http.d.ts +9 -0
  223. package/dist/types/src/operations/sources/http.d.ts.map +1 -0
  224. package/dist/types/src/operations/sources/index.d.ts +6 -0
  225. package/dist/types/src/operations/sources/index.d.ts.map +1 -0
  226. package/dist/types/src/operations/sources/rss.d.ts +4 -0
  227. package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
  228. package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
  229. package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
  230. package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
  231. package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
  232. package/dist/types/src/operations/sync-feed.d.ts +5 -0
  233. package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
  234. package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
  235. package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
  236. package/dist/types/src/operations/util.d.ts +25 -0
  237. package/dist/types/src/operations/util.d.ts.map +1 -0
  238. package/dist/types/src/operations/util.test.d.ts +2 -0
  239. package/dist/types/src/operations/util.test.d.ts.map +1 -0
  240. package/dist/types/src/paths.d.ts +3 -0
  241. package/dist/types/src/paths.d.ts.map +1 -0
  242. package/dist/types/src/plugin.d.ts +4 -0
  243. package/dist/types/src/plugin.d.ts.map +1 -0
  244. package/dist/types/src/plugin.workerd.d.ts +3 -0
  245. package/dist/types/src/plugin.workerd.d.ts.map +1 -0
  246. package/dist/types/src/skills/index.d.ts +2 -0
  247. package/dist/types/src/skills/index.d.ts.map +1 -0
  248. package/dist/types/src/skills/magazine-skill.d.ts +4 -0
  249. package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
  250. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
  251. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
  252. package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
  253. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
  254. package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
  255. package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
  256. package/dist/types/src/templates/magazine-curation.d.ts +7 -0
  257. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
  258. package/dist/types/src/testing/builder.d.ts +28 -0
  259. package/dist/types/src/testing/builder.d.ts.map +1 -0
  260. package/dist/types/src/testing/index.d.ts +2 -0
  261. package/dist/types/src/testing/index.d.ts.map +1 -0
  262. package/dist/types/src/translations.d.ts +247 -0
  263. package/dist/types/src/translations.d.ts.map +1 -0
  264. package/dist/types/src/types/CreateSubscription.d.ts +29 -0
  265. package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
  266. package/dist/types/src/types/FeedOperation.d.ts +52 -0
  267. package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
  268. package/dist/types/src/types/Magazine.d.ts +87 -0
  269. package/dist/types/src/types/Magazine.d.ts.map +1 -0
  270. package/dist/types/src/types/Magazine.test.d.ts +2 -0
  271. package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
  272. package/dist/types/src/types/Subscription.d.ts +144 -0
  273. package/dist/types/src/types/Subscription.d.ts.map +1 -0
  274. package/dist/types/src/types/Subscription.test.d.ts +2 -0
  275. package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
  276. package/dist/types/src/types/index.d.ts +5 -0
  277. package/dist/types/src/types/index.d.ts.map +1 -0
  278. package/dist/types/src/util/date.d.ts +8 -0
  279. package/dist/types/src/util/date.d.ts.map +1 -0
  280. package/dist/types/src/util/index.d.ts +4 -0
  281. package/dist/types/src/util/index.d.ts.map +1 -0
  282. package/dist/types/src/util/post-content.d.ts +14 -0
  283. package/dist/types/src/util/post-content.d.ts.map +1 -0
  284. package/dist/types/src/util/text.d.ts +18 -0
  285. package/dist/types/src/util/text.d.ts.map +1 -0
  286. package/dist/types/tsconfig.tsbuildinfo +1 -0
  287. package/dx.config.ts +48 -0
  288. package/package.json +164 -0
  289. package/src/MagazinePlugin.test.ts +36 -0
  290. package/src/MagazinePlugin.tsx +61 -0
  291. package/src/MagazinePlugin.workerd.ts +31 -0
  292. package/src/atoms/atoms.test.ts +323 -0
  293. package/src/atoms/index.ts +10 -0
  294. package/src/atoms/magazine-posts.ts +70 -0
  295. package/src/atoms/post-content.ts +34 -0
  296. package/src/atoms/post-curation.ts +36 -0
  297. package/src/atoms/post-display.ts +59 -0
  298. package/src/atoms/post-read.ts +40 -0
  299. package/src/atoms/post-tags.ts +66 -0
  300. package/src/capabilities/app-graph-builder.ts +125 -0
  301. package/src/capabilities/create-object.ts +127 -0
  302. package/src/capabilities/index.ts +17 -0
  303. package/src/capabilities/navigation-resolver.ts +21 -0
  304. package/src/capabilities/operation-handler.ts +16 -0
  305. package/src/capabilities/react-surface.tsx +53 -0
  306. package/src/capabilities/routine-templates.ts +16 -0
  307. package/src/capabilities/skill-definition.ts +14 -0
  308. package/src/components/PostContent/PostContent.test.ts +81 -0
  309. package/src/components/PostContent/PostContent.tsx +111 -0
  310. package/src/components/PostContent/index.ts +5 -0
  311. package/src/components/PostStack/PostStack.stories.tsx +39 -0
  312. package/src/components/PostStack/PostStack.tsx +151 -0
  313. package/src/components/PostStack/index.ts +5 -0
  314. package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
  315. package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
  316. package/src/components/SubscriptionStack/index.ts +5 -0
  317. package/src/components/index.ts +7 -0
  318. package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
  319. package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
  320. package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
  321. package/src/containers/FeedArticle/index.ts +5 -0
  322. package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
  323. package/src/containers/FeedProperties/index.ts +5 -0
  324. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
  325. package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
  326. package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
  327. package/src/containers/MagazineArticle/index.ts +5 -0
  328. package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
  329. package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
  330. package/src/containers/PostArticle/PostArticle.tsx +140 -0
  331. package/src/containers/PostArticle/PostToolbar.tsx +120 -0
  332. package/src/containers/PostArticle/index.ts +5 -0
  333. package/src/containers/PostCard/PostCard.tsx +70 -0
  334. package/src/containers/PostCard/index.ts +5 -0
  335. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
  336. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
  337. package/src/containers/SubscriptionsArticle/index.ts +5 -0
  338. package/src/containers/index.ts +11 -0
  339. package/src/index.ts +6 -0
  340. package/src/meta.ts +9 -0
  341. package/src/operations/clear-magazine.ts +83 -0
  342. package/src/operations/curate-magazine.skill.conversations.json +1 -0
  343. package/src/operations/curate-magazine.skill.test.ts +167 -0
  344. package/src/operations/curate-magazine.test.ts +91 -0
  345. package/src/operations/curate-magazine.ts +178 -0
  346. package/src/operations/extraction/article.test.ts +154 -0
  347. package/src/operations/extraction/article.ts +270 -0
  348. package/src/operations/extraction/index.ts +5 -0
  349. package/src/operations/fetch-article-content.ts +27 -0
  350. package/src/operations/index.ts +13 -0
  351. package/src/operations/load-post-content.ts +76 -0
  352. package/src/operations/sources/article.ts +122 -0
  353. package/src/operations/sources/cors.ts +19 -0
  354. package/src/operations/sources/feed-fetcher.test.ts +200 -0
  355. package/src/operations/sources/feed-fetcher.ts +26 -0
  356. package/src/operations/sources/http.ts +42 -0
  357. package/src/operations/sources/index.ts +9 -0
  358. package/src/operations/sources/rss.test.ts +201 -0
  359. package/src/operations/sources/rss.ts +138 -0
  360. package/src/operations/sources/standard-site.ts +381 -0
  361. package/src/operations/sources/testing/feed.xml +477 -0
  362. package/src/operations/sync-feed.test.ts +146 -0
  363. package/src/operations/sync-feed.ts +140 -0
  364. package/src/operations/util.test.ts +67 -0
  365. package/src/operations/util.ts +65 -0
  366. package/src/paths.ts +14 -0
  367. package/src/plugin.ts +11 -0
  368. package/src/plugin.workerd.ts +6 -0
  369. package/src/skills/index.ts +5 -0
  370. package/src/skills/magazine-skill.ts +45 -0
  371. package/src/stories/ArticleExtractor.stories.tsx +204 -0
  372. package/src/stories/MagazineCurate.stories.tsx +217 -0
  373. package/src/stories/fixtures/theregister-ai.xml +45 -0
  374. package/src/stories/theregister-fixture.test.ts +35 -0
  375. package/src/templates/magazine-curation.ts +47 -0
  376. package/src/testing/builder.ts +77 -0
  377. package/src/testing/index.ts +5 -0
  378. package/src/translations.ts +81 -0
  379. package/src/types/CreateSubscription.ts +61 -0
  380. package/src/types/FeedOperation.ts +134 -0
  381. package/src/types/Magazine.test.ts +69 -0
  382. package/src/types/Magazine.ts +214 -0
  383. package/src/types/Subscription.test.ts +171 -0
  384. package/src/types/Subscription.ts +366 -0
  385. package/src/types/index.ts +8 -0
  386. package/src/util/date.ts +27 -0
  387. package/src/util/index.ts +7 -0
  388. package/src/util/post-content.ts +19 -0
  389. package/src/util/text.ts +98 -0
  390. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,200 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { afterEach, beforeEach, describe, test, vi } from 'vitest';
6
+
7
+ import { EffectEx } from '@dxos/effect';
8
+
9
+ import { fetchStandardSite, listStandardSitePublications, parseStandardSiteActor } from './standard-site';
10
+
11
+ // The Standard.site fetchers are Effects (they provide their own HTTP layer); run them for assertions.
12
+ // `fetchStandardSite` now takes the publication site reference (`at://` or `https://`) as `url`.
13
+ const runFetchStandardSite = (url: string) => EffectEx.runPromise(fetchStandardSite(url));
14
+
15
+ // RSS/Atom coverage lives in rss.test.ts.
16
+
17
+ // Sample Standard.site read chain: resolveHandle → plc.directory (DID doc) → listRecords → getProfile →
18
+ // getRecord (publication). One document references its publication by `at://` URI, the other by a bare
19
+ // `https://` URL (resolved directly, no getRecord needed).
20
+ const DID = 'did:plc:abc123';
21
+ const PDS = 'https://pds.example.com';
22
+ const PUBLICATION_URI = `at://${DID}/site.standard.publication/self`;
23
+
24
+ const DID_DOCUMENT = {
25
+ service: [{ id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: PDS }],
26
+ };
27
+
28
+ const PROFILE = {
29
+ did: DID,
30
+ handle: 'alice.example.com',
31
+ displayName: 'Alice',
32
+ avatar: 'https://example.com/avatar.jpg',
33
+ description: 'Long-form writing.',
34
+ };
35
+
36
+ const PUBLICATION = {
37
+ uri: PUBLICATION_URI,
38
+ value: { url: 'https://alice.example.com/', name: 'Alice Blog' },
39
+ };
40
+
41
+ const LIST_RECORDS = {
42
+ records: [
43
+ {
44
+ uri: `at://${DID}/site.standard.document/doc1`,
45
+ value: {
46
+ site: PUBLICATION_URI,
47
+ title: 'First Article',
48
+ path: '/blog/first',
49
+ description: 'A first long-form post.',
50
+ content: { $type: 'site.standard.content.markdown', text: '# First\n\nBody text.', version: '1.0' },
51
+ textContent: 'First Body text.',
52
+ publishedAt: '2026-01-02T00:00:00Z',
53
+ tags: ['echo'],
54
+ },
55
+ },
56
+ {
57
+ uri: `at://${DID}/site.standard.document/doc2`,
58
+ value: {
59
+ site: 'https://alice.example.com',
60
+ title: 'Older Article',
61
+ path: '/blog/older',
62
+ content: { $type: 'site.standard.content.markdown', text: '# Older' },
63
+ publishedAt: '2026-01-01T00:00:00Z',
64
+ },
65
+ },
66
+ ],
67
+ };
68
+
69
+ describe('FeedFetcher', () => {
70
+ let originalFetch: typeof globalThis.fetch;
71
+
72
+ beforeEach(() => {
73
+ originalFetch = globalThis.fetch;
74
+ });
75
+
76
+ afterEach(() => {
77
+ globalThis.fetch = originalFetch;
78
+ vi.restoreAllMocks();
79
+ });
80
+
81
+ describe('fetchStandardSite', () => {
82
+ test('takes an at:// publication reference, fetches only matching documents', async ({ expect }) => {
83
+ globalThis.fetch = routedFetch();
84
+
85
+ // `url` is the publication site reference; DID is extracted directly from the at:// URI.
86
+ const result = await runFetchStandardSite(PUBLICATION_URI);
87
+
88
+ expect(result.feed.type).toBe('standard-site');
89
+ // Feed name and url come from the publication record; icon/description from the author profile.
90
+ expect(result.feed.name).toBe('Alice Blog');
91
+ expect(result.feed.url).toBe(PUBLICATION_URI);
92
+ expect(result.feed.iconUrl).toBe('https://example.com/avatar.jpg');
93
+ expect(result.feed.description).toBe('Long-form writing.');
94
+
95
+ // Only the document whose `site` matches PUBLICATION_URI is included.
96
+ expect(result.posts).toHaveLength(1);
97
+ const [first] = result.posts;
98
+ expect(first.title).toBe('First Article');
99
+ expect(first.link).toBe('https://alice.example.com/blog/first');
100
+ expect(first.content).toBe('# First\n\nBody text.');
101
+ expect(first.description).toBe('A first long-form post.');
102
+ expect(first.author).toBe('Alice');
103
+ expect(first.published).toBe('2026-01-02T00:00:00Z');
104
+ expect(first.guid).toBe(`at://${DID}/site.standard.document/doc1`);
105
+ });
106
+
107
+ test('takes an https:// publication reference, resolves DID via /.well-known/atproto-did', async ({ expect }) => {
108
+ const mockFetch = routedFetch();
109
+ globalThis.fetch = mockFetch;
110
+
111
+ // The https:// site reference triggers a /.well-known/atproto-did lookup for the DID.
112
+ const result = await runFetchStandardSite('https://alice.example.com');
113
+
114
+ const calls = mockFetch.mock.calls.map((call) => String(call[0]));
115
+ expect(calls.some((url) => url.includes('/.well-known/atproto-did'))).toBe(true);
116
+ expect(calls.some((url) => url.includes('resolveHandle'))).toBe(false);
117
+
118
+ // The document whose `site` is 'https://alice.example.com' is the only match.
119
+ expect(result.posts).toHaveLength(1);
120
+ expect(result.posts[0].title).toBe('Older Article');
121
+ });
122
+
123
+ test('fails on a non-ok response', async ({ expect }) => {
124
+ globalThis.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 404, statusText: 'Not Found' }));
125
+
126
+ await expect(runFetchStandardSite(PUBLICATION_URI)).rejects.toThrow(/Fetch failed/);
127
+ });
128
+ });
129
+
130
+ describe('listStandardSitePublications', () => {
131
+ test('lists the distinct publications a handle publishes under', async ({ expect }) => {
132
+ globalThis.fetch = routedFetch();
133
+
134
+ const publications = await EffectEx.runPromise(listStandardSitePublications('alice.example.com'));
135
+
136
+ // One `at://` publication (resolved via getRecord) and one bare `https://` site.
137
+ expect(publications).toHaveLength(2);
138
+ const bySite = new Map(publications.map((publication) => [publication.site, publication]));
139
+ expect(bySite.get(PUBLICATION_URI)?.name).toBe('Alice Blog');
140
+ expect(bySite.get('https://alice.example.com')?.url).toBe('https://alice.example.com');
141
+ });
142
+ });
143
+
144
+ describe('parseStandardSiteActor', () => {
145
+ test('extracts handle from bsky.app URL', ({ expect }) => {
146
+ expect(parseStandardSiteActor('https://bsky.app/profile/alice.bsky.social')).toBe('alice.bsky.social');
147
+ });
148
+
149
+ test('extracts handle from bsky.app URL with trailing path', ({ expect }) => {
150
+ expect(parseStandardSiteActor('https://bsky.app/profile/alice.bsky.social/post/abc')).toBe('alice.bsky.social');
151
+ });
152
+
153
+ test('passes through bare handle', ({ expect }) => {
154
+ expect(parseStandardSiteActor('dxos.org')).toBe('dxos.org');
155
+ });
156
+
157
+ test('strips leading @ from handle', ({ expect }) => {
158
+ expect(parseStandardSiteActor('@alice.bsky.social')).toBe('alice.bsky.social');
159
+ });
160
+
161
+ test('passes through DID', ({ expect }) => {
162
+ expect(parseStandardSiteActor('did:plc:abc123')).toBe('did:plc:abc123');
163
+ });
164
+ });
165
+ });
166
+
167
+ // Helpers
168
+
169
+ const okJson = (data: unknown) =>
170
+ new Response(JSON.stringify(data), { status: 200, headers: { 'content-type': 'application/json' } });
171
+
172
+ /**
173
+ * Routes a mocked `fetch` by endpoint so the resolution chain is order-independent.
174
+ * `fetchStandardSite` takes a site reference as `url`:
175
+ * - `at://did:plc:abc123/…` → extract DID → plc.directory → listRecords → getProfile → getRecord
176
+ * - `https://alice.example.com` → /.well-known/atproto-did → plc.directory → listRecords → getProfile
177
+ */
178
+ const routedFetch = () =>
179
+ vi.fn(async (input: unknown) => {
180
+ const url = String(input);
181
+ if (url.includes('/.well-known/atproto-did')) {
182
+ return okJson({ did: DID });
183
+ }
184
+ if (url.includes('resolveHandle')) {
185
+ return okJson({ did: DID });
186
+ }
187
+ if (url.includes('plc.directory')) {
188
+ return okJson(DID_DOCUMENT);
189
+ }
190
+ if (url.includes('listRecords')) {
191
+ return okJson(LIST_RECORDS);
192
+ }
193
+ if (url.includes('getProfile')) {
194
+ return okJson(PROFILE);
195
+ }
196
+ if (url.includes('getRecord')) {
197
+ return okJson(PUBLICATION);
198
+ }
199
+ throw new Error(`Unexpected fetch URL: ${url}`);
200
+ });
@@ -0,0 +1,26 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Data from 'effect/Data';
6
+ import type * as Effect from 'effect/Effect';
7
+
8
+ import { type Subscription } from '#types';
9
+
10
+ export type FetchOptions = {
11
+ corsProxy?: string;
12
+ };
13
+
14
+ export type FetchResult = {
15
+ feed: Subscription.Subscription;
16
+ posts: Subscription.Post[];
17
+ };
18
+
19
+ /** Failure fetching or decoding a feed (network error, non-2xx response, or malformed body). */
20
+ export class FeedFetchError extends Data.TaggedError('FeedFetchError')<{
21
+ message: string;
22
+ cause?: unknown;
23
+ }> {}
24
+
25
+ /** Generic feed fetcher — protocol-specific implementations return normalized Subscription objects. */
26
+ export type FeedFetcher = (url: string, options?: FetchOptions) => Effect.Effect<FetchResult, FeedFetchError>;
@@ -0,0 +1,42 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as HttpClient from '@effect/platform/HttpClient';
6
+ import * as HttpClientRequest from '@effect/platform/HttpClientRequest';
7
+ import * as HttpClientResponse from '@effect/platform/HttpClientResponse';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Schedule from 'effect/Schedule';
10
+ import type * as Schema from 'effect/Schema';
11
+
12
+ import { applyCorsProxy } from './cors';
13
+ import { FeedFetchError } from './feed-fetcher';
14
+
15
+ // Short, bounded retry so a transient hiccup doesn't fail a fetch, without stalling the form.
16
+ const retryPolicy = Schedule.exponential('500 millis').pipe(Schedule.compose(Schedule.recurs(2)));
17
+
18
+ /** GETs a URL (through the optional CORS proxy) and decodes the JSON body against `schema`. */
19
+ export const getJson = <A, I>(
20
+ schema: Schema.Schema<A, I>,
21
+ url: string,
22
+ proxy?: string,
23
+ ): Effect.Effect<A, FeedFetchError, HttpClient.HttpClient> =>
24
+ HttpClientRequest.get(applyCorsProxy(url, proxy)).pipe(
25
+ HttpClient.execute,
26
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)),
27
+ Effect.timeout('10 seconds'),
28
+ Effect.retry(retryPolicy),
29
+ Effect.scoped,
30
+ Effect.mapError((cause) => new FeedFetchError({ message: `Fetch failed: ${url}`, cause })),
31
+ );
32
+
33
+ /** GETs a URL (through the optional CORS proxy) and returns the response body as text. */
34
+ export const getText = (url: string, proxy?: string): Effect.Effect<string, FeedFetchError, HttpClient.HttpClient> =>
35
+ HttpClientRequest.get(applyCorsProxy(url, proxy)).pipe(
36
+ HttpClient.execute,
37
+ Effect.flatMap((response) => response.text),
38
+ Effect.timeout('10 seconds'),
39
+ Effect.retry(retryPolicy),
40
+ Effect.scoped,
41
+ Effect.mapError((cause) => new FeedFetchError({ message: `Fetch failed: ${url}`, cause })),
42
+ );
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './article';
6
+ export * from './cors';
7
+ export * from './feed-fetcher';
8
+ export * from './rss';
9
+ export * from './standard-site';
@@ -0,0 +1,201 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { afterEach, beforeEach, describe, test, vi } from 'vitest';
6
+
7
+ import { EffectEx } from '@dxos/effect';
8
+
9
+ import { fetchRss } from './rss';
10
+ // Real-world RSS feed (CDATA-wrapped summary + content:encoded HTML), loaded via Vite's `?raw` suffix.
11
+ import FEED_XML from './testing/feed.xml?raw';
12
+
13
+ // `fetchRss` is an Effect (it provides its own HTTP layer); run it to a promise for assertions.
14
+ const runFetchRss = (url: string, options?: { corsProxy?: string }) => EffectEx.runPromise(fetchRss(url, options));
15
+
16
+ // Sample RSS XML fixture.
17
+ const RSS_XML = `<?xml version="1.0" encoding="UTF-8"?>
18
+ <rss version="2.0">
19
+ <channel>
20
+ <title>Test RSS Feed</title>
21
+ <description>A test feed.</description>
22
+ <link>https://example.com</link>
23
+ <item>
24
+ <title>First Post</title>
25
+ <link>https://example.com/post-1</link>
26
+ <description>Description of first post.</description>
27
+ <author>Alice</author>
28
+ <pubDate>2025-01-15T12:00:00Z</pubDate>
29
+ <guid>post-1</guid>
30
+ </item>
31
+ <item>
32
+ <title>Second Post</title>
33
+ <link>https://example.com/post-2</link>
34
+ <description>Description of second post.</description>
35
+ <author>Bob</author>
36
+ <pubDate>2025-01-14T12:00:00Z</pubDate>
37
+ <guid>post-2</guid>
38
+ </item>
39
+ </channel>
40
+ </rss>`;
41
+
42
+ // Sample Atom XML fixture.
43
+ const ATOM_XML = `<?xml version="1.0" encoding="UTF-8"?>
44
+ <feed xmlns="http://www.w3.org/2005/Atom">
45
+ <title>Test Atom Feed</title>
46
+ <subtitle>An Atom test feed.</subtitle>
47
+ <entry>
48
+ <title>Atom Entry One</title>
49
+ <link rel="alternate" href="https://example.com/atom-1"/>
50
+ <summary>Summary of atom entry one.</summary>
51
+ <author><name>Charlie</name></author>
52
+ <published>2025-02-01T10:00:00Z</published>
53
+ <id>atom-entry-1</id>
54
+ </entry>
55
+ </feed>`;
56
+
57
+ const mockFetchOnce = (body: string) => {
58
+ globalThis.fetch = vi
59
+ .fn()
60
+ .mockResolvedValue(new Response(body, { status: 200, headers: { 'content-type': 'application/rss+xml' } }));
61
+ };
62
+
63
+ describe('fetchRss', () => {
64
+ let originalFetch: typeof globalThis.fetch;
65
+
66
+ beforeEach(() => {
67
+ originalFetch = globalThis.fetch;
68
+ });
69
+
70
+ afterEach(() => {
71
+ globalThis.fetch = originalFetch;
72
+ vi.restoreAllMocks();
73
+ });
74
+
75
+ test('parses RSS feed into Subscription objects', async ({ expect }) => {
76
+ mockFetchOnce(RSS_XML);
77
+
78
+ const result = await runFetchRss('https://example.com/rss');
79
+
80
+ expect(result.feed.name).toBe('Test RSS Feed');
81
+ expect(result.feed.description).toBe('A test feed.');
82
+ expect(result.posts).toHaveLength(2);
83
+ expect(result.posts[0].title).toBe('First Post');
84
+ expect(result.posts[0].author).toBe('Alice');
85
+ expect(result.posts[0].guid).toBe('post-1');
86
+ expect(result.posts[1].title).toBe('Second Post');
87
+ expect(result.posts[1].guid).toBe('post-2');
88
+ });
89
+
90
+ test('parses Atom feed into Subscription objects', async ({ expect }) => {
91
+ mockFetchOnce(ATOM_XML);
92
+
93
+ const result = await runFetchRss('https://example.com/atom');
94
+
95
+ expect(result.feed.name).toBe('Test Atom Feed');
96
+ expect(result.feed.description).toBe('An Atom test feed.');
97
+ expect(result.posts).toHaveLength(1);
98
+ expect(result.posts[0].title).toBe('Atom Entry One');
99
+ expect(result.posts[0].author).toBe('Charlie');
100
+ expect(result.posts[0].guid).toBe('atom-entry-1');
101
+ expect(result.posts[0].link).toBe('https://example.com/atom-1');
102
+ });
103
+
104
+ test('extracts text from RSS elements that carry attributes', async ({ expect }) => {
105
+ const xmlWithAttrs = `<?xml version="1.0" encoding="UTF-8"?>
106
+ <rss version="2.0">
107
+ <channel>
108
+ <title type="text">Feed With Attrs</title>
109
+ <description xml:lang="en">Feed description.</description>
110
+ <item>
111
+ <title type="html">Post With Attrs</title>
112
+ <link>https://example.com/post-3</link>
113
+ <description type="html">Post &amp; body.</description>
114
+ <author>Dana</author>
115
+ <pubDate>2025-05-10T12:00:00Z</pubDate>
116
+ <guid isPermaLink="false">post-3</guid>
117
+ </item>
118
+ </channel>
119
+ </rss>`;
120
+ mockFetchOnce(xmlWithAttrs);
121
+
122
+ const result = await runFetchRss('https://example.com/rss');
123
+
124
+ expect(result.feed.name).toBe('Feed With Attrs');
125
+ expect(result.feed.description).toBe('Feed description.');
126
+ expect(result.posts).toHaveLength(1);
127
+ expect(result.posts[0].title).toBe('Post With Attrs');
128
+ expect(result.posts[0].description).toBe('Post & body.');
129
+ expect(result.posts[0].author).toBe('Dana');
130
+ expect(result.posts[0].guid).toBe('post-3');
131
+ expect(result.posts[0].link).toBe('https://example.com/post-3');
132
+ });
133
+
134
+ test('handles deeply nested HTML in description without exceeding parser limits', async ({ expect }) => {
135
+ // Build nested HTML 200 levels deep — well past fast-xml-parser's default 100-tag cap.
136
+ let nested = 'leaf';
137
+ for (let index = 0; index < 200; index++) {
138
+ nested = `<div>${nested}</div>`;
139
+ }
140
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
141
+ <rss version="2.0">
142
+ <channel>
143
+ <title>Nested HTML Feed</title>
144
+ <description>Feed.</description>
145
+ <item>
146
+ <title>Nested Post</title>
147
+ <link>https://example.com/nested</link>
148
+ <description>${nested}</description>
149
+ <guid>nested-1</guid>
150
+ </item>
151
+ </channel>
152
+ </rss>`;
153
+ mockFetchOnce(xml);
154
+
155
+ const result = await runFetchRss('https://example.com/rss');
156
+
157
+ expect(result.posts).toHaveLength(1);
158
+ expect(result.posts[0].title).toBe('Nested Post');
159
+ expect(result.posts[0].guid).toBe('nested-1');
160
+ });
161
+
162
+ test('uses CORS proxy when provided', async ({ expect }) => {
163
+ const mockFetch = vi
164
+ .fn()
165
+ .mockResolvedValue(new Response(RSS_XML, { status: 200, headers: { 'content-type': 'application/rss+xml' } }));
166
+ globalThis.fetch = mockFetch;
167
+
168
+ await runFetchRss('https://example.com/rss', { corsProxy: '/api/rss?url=' });
169
+
170
+ // FetchHttpClient calls `fetch(url, init)`; the test env resolves the relative proxy path to an
171
+ // absolute URL, so assert the proxied target is contained rather than matching exactly.
172
+ expect(String(mockFetch.mock.calls[0][0])).toContain(
173
+ '/api/rss?url=' + encodeURIComponent('https://example.com/rss'),
174
+ );
175
+ });
176
+
177
+ test('parses a real-world RSS feed, unwrapping CDATA and converting content to markdown', async ({ expect }) => {
178
+ mockFetchOnce(FEED_XML);
179
+
180
+ const result = await runFetchRss('https://www.theregister.com/headlines.atom');
181
+
182
+ expect(result.feed.name).toBe('www.theregister.com - Articles');
183
+ expect(result.feed.description).toBe('Articles from www.theregister.com');
184
+ expect(result.posts).toHaveLength(25);
185
+
186
+ const post = result.posts[0];
187
+ expect(post.title).toBe("Trump's AI E-(I)-O could let feds pick winners and losers");
188
+ expect(post.link).toBe(
189
+ 'https://www.theregister.com/ai-and-ml/2026/06/02/trump-ai-executive-order-sets-30-day-frontier-model-review/5250322',
190
+ );
191
+ expect(post.guid).toBe('https://www.theregister.com/a/5250322');
192
+
193
+ // CDATA-wrapped <description> summary is unwrapped (and trimmed).
194
+ expect(post.description).toBe("Government gets a say in 'trusted partner' access, and that worries policy experts");
195
+
196
+ // <content:encoded> HTML is unwrapped from CDATA and converted to markdown — no raw block tags survive.
197
+ expect(post.content).toBeDefined();
198
+ expect(post.content).toContain('After postponing a planned signing');
199
+ expect(post.content).not.toMatch(/<\/?(p|div|h[1-6]|ul|ol|li|a|span)\b/i);
200
+ });
201
+ });
@@ -0,0 +1,138 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import { XMLParser } from 'fast-xml-parser';
8
+
9
+ import { normalizeText } from '@dxos/markdown';
10
+
11
+ import { Subscription } from '#types';
12
+
13
+ import { decodeEntities } from '../../util/text';
14
+ import { type FeedFetcher, FeedFetchError, type FetchResult } from './feed-fetcher';
15
+ import { getText } from './http';
16
+
17
+ /**
18
+ * Unwrap `<![CDATA[ ... ]]>` sections, returning the inner content.
19
+ * fast-xml-parser returns stopNode'd content (description / summary / content / content:encoded)
20
+ * verbatim, so CDATA-wrapped HTML — which feeds use pervasively — arrives as a literal
21
+ * `<![CDATA[ ... ]]>` string. Strip the wrapper(s) so the inner HTML reaches downstream
22
+ * conversion. A no-op for values that contain no CDATA section.
23
+ */
24
+ const stripCdata = (value: string): string => value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
25
+
26
+ /**
27
+ * Normalize a fast-xml-parser value to a plain string.
28
+ * fast-xml-parser yields objects like `{'#text': 'foo', '@_type': 'html'}` for
29
+ * elements with attributes, and plain strings/numbers for text-only elements.
30
+ * Returns undefined for nullish values.
31
+ *
32
+ * Entities are decoded explicitly: stopNode'd nodes (description / content /
33
+ * summary / content:encoded) are returned as raw text by fast-xml-parser,
34
+ * skipping its built-in entity decoding and CDATA unwrapping. Decoding and
35
+ * CDATA stripping here keep callers from having to special-case those fields.
36
+ * For non-stopped nodes both are no-ops (the parser has already decoded
37
+ * entities and there is no CDATA wrapper).
38
+ */
39
+ const text = (value: unknown): string | undefined => {
40
+ if (value == null) {
41
+ return undefined;
42
+ }
43
+ if (typeof value === 'string') {
44
+ return decodeEntities(stripCdata(value));
45
+ }
46
+ if (typeof value === 'number' || typeof value === 'boolean') {
47
+ return String(value);
48
+ }
49
+ if (typeof value === 'object') {
50
+ const t = (value as { '#text'?: unknown })['#text'];
51
+ if (typeof t === 'string') {
52
+ return decodeEntities(stripCdata(t));
53
+ }
54
+ if (typeof t === 'number' || typeof t === 'boolean') {
55
+ return String(t);
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ };
61
+
62
+ /**
63
+ * Convert a feed text value to Markdown. Feed description/content/summary fields routinely carry
64
+ * embedded HTML (escaped or raw); normalizeText converts it to Markdown and passes plaintext
65
+ * through unchanged. Returns undefined for nullish values.
66
+ */
67
+ const markdown = (value: string | undefined): string | undefined => (value != null ? normalizeText(value) : undefined);
68
+
69
+ /** Fetches and parses an RSS/Atom feed URL into Subscription objects. */
70
+ export const fetchRss: FeedFetcher = (url, options) =>
71
+ Effect.gen(function* () {
72
+ const xml = yield* getText(url, options?.corsProxy);
73
+ return yield* Effect.try({
74
+ try: () => parseFeed(url, xml),
75
+ catch: (cause) => new FeedFetchError({ message: `Unrecognized feed format: ${url}`, cause }),
76
+ });
77
+ }).pipe(Effect.provide(FetchHttpClient.layer));
78
+
79
+ /** Parses RSS/Atom XML into a normalized {@link FetchResult}; throws on an unrecognized shape. */
80
+ const parseFeed = (url: string, xml: string): FetchResult => {
81
+ const parser = new XMLParser({
82
+ ignoreAttributes: false,
83
+ attributeNamePrefix: '@_',
84
+ // RSS/Atom feeds frequently embed HTML (sometimes un-escaped, without CDATA) in content fields,
85
+ // which can blow past fast-xml-parser's default 100-tag nesting cap.
86
+ maxNestedTags: 10_000,
87
+ // Treat known HTML-bearing fields as opaque text so embedded markup isn't parsed as XML.
88
+ stopNodes: ['*.description', '*.summary', '*.content', '*.content:encoded'],
89
+ });
90
+
91
+ const parsed = parser.parse(xml);
92
+ const channel = parsed.rss?.channel ?? parsed.feed;
93
+ if (!channel) {
94
+ throw new Error('Unrecognized feed format');
95
+ }
96
+
97
+ const isAtom = !parsed.rss;
98
+ const feedName = text(channel.title) ?? '';
99
+ const feedDescription = markdown(text(isAtom ? channel.subtitle : channel.description)) ?? '';
100
+
101
+ const items: any[] = (isAtom ? channel.entry : channel.item) ?? [];
102
+ const itemList = Array.isArray(items) ? items : [items];
103
+
104
+ const posts = itemList.map((item) => {
105
+ const link = isAtom
106
+ ? ((Array.isArray(item.link)
107
+ ? item.link.find((l: any) => l['@_rel'] === 'alternate')?.['@_href']
108
+ : (item.link?.['@_href'] ?? text(item.link))) ?? '')
109
+ : (text(item.link) ?? '');
110
+
111
+ const author = isAtom
112
+ ? (text(item.author?.name) ?? text(item.author))
113
+ : (text(item['dc:creator']) ?? text(item.author));
114
+
115
+ const description = markdown(text(item.description));
116
+ const content = isAtom
117
+ ? (markdown(text(item.summary)) ?? markdown(text(item.content)))
118
+ : markdown(text(item['content:encoded']));
119
+
120
+ return Subscription.makePost({
121
+ title: text(item.title),
122
+ link,
123
+ description,
124
+ content,
125
+ author,
126
+ published: text(item.pubDate) ?? text(item.published) ?? text(item.updated),
127
+ guid: (isAtom ? text(item.id) : text(item.guid)) ?? link,
128
+ });
129
+ });
130
+
131
+ const feed = Subscription.makeSubscription({
132
+ name: feedName,
133
+ url,
134
+ description: feedDescription,
135
+ });
136
+
137
+ return { feed, posts };
138
+ };