six-degrees 0.4.7 → 0.4.9
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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +2 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js +2 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +2 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +3 -2
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +2 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/route.js +4 -2
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{2_zu7rmb8flzl.js → 04bbi55urkrd6.js} +1 -1
- package/.next/{static/chunks/3q3pmt5vv-4pi.js → standalone/.next/static/chunks/054zdthcnyd2h.js} +1 -1
- package/.next/{static/chunks/236f0jtp68j_h.js → standalone/.next/static/chunks/1_mohm2643nxp.js} +1 -1
- package/.next/{static/chunks/3zw8bv7-mc4n6.js → standalone/.next/static/chunks/1z_rt4jteewqp.js} +1 -1
- package/.next/standalone/.next/static/chunks/24tvdun1cb3g1.js +1 -0
- package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +1 -0
- package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
- package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +1 -0
- package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
- package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
- package/.next/standalone/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
- package/.next/standalone/CHANGELOG.md +72 -0
- package/.next/standalone/CONTRIBUTING.md +5 -0
- package/.next/standalone/README.md +168 -56
- package/.next/standalone/app/api/social/messages/route.js +130 -0
- package/.next/standalone/app/api/social/route.js +89 -47
- package/.next/standalone/app/components/ScanStatusBar.js +40 -3
- package/.next/standalone/app/components/settings/DataSection.js +2 -1
- package/.next/standalone/app/favicon.ico +0 -0
- package/.next/standalone/app/paths/page.js +27 -18
- package/.next/standalone/app/profile/page.js +2 -0
- package/.next/standalone/app/queue/page.js +2 -0
- package/.next/standalone/app/scores/page.js +2 -0
- package/.next/standalone/app/settings/page.js +2 -0
- package/.next/standalone/app/setup/page.js +6 -3
- package/.next/standalone/app/social/Conversations.js +246 -0
- package/.next/standalone/app/social/page.js +90 -9
- package/.next/standalone/lib/galaxy-lab.js +6 -1
- package/.next/standalone/lib/linkedin-export.js +175 -5
- package/.next/standalone/lib/scoring.js +42 -1
- package/.next/standalone/lib/social-store.js +73 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/build-site.mjs +537 -0
- package/.next/standalone/scripts/scrape.py +156 -23
- package/.next/standalone/scripts/site-markdown.mjs +188 -0
- package/.next/standalone/scripts/site-news.mjs +311 -0
- package/.next/static/chunks/{2_zu7rmb8flzl.js → 04bbi55urkrd6.js} +1 -1
- package/.next/{standalone/.next/static/chunks/3q3pmt5vv-4pi.js → static/chunks/054zdthcnyd2h.js} +1 -1
- package/.next/{standalone/.next/static/chunks/236f0jtp68j_h.js → static/chunks/1_mohm2643nxp.js} +1 -1
- package/.next/{standalone/.next/static/chunks/3zw8bv7-mc4n6.js → static/chunks/1z_rt4jteewqp.js} +1 -1
- package/.next/static/chunks/24tvdun1cb3g1.js +1 -0
- package/.next/static/chunks/2bftn9gubwf0w.js +1 -0
- package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
- package/.next/static/chunks/39csdcx7yv227.js +1 -0
- package/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
- package/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
- package/.next/static/media/favicon.23vm2o6v3er2z.ico +0 -0
- package/CHANGELOG.md +72 -0
- package/README.md +168 -56
- package/lib/galaxy-lab.js +6 -1
- package/lib/linkedin-export.js +175 -5
- package/lib/scoring.js +42 -1
- package/lib/social-store.js +73 -0
- package/package.json +1 -1
- package/scripts/scrape.py +156 -23
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
- package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
- package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
- package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
- package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
- package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
- package/.next/static/chunks/14m1zu-8warte.js +0 -1
- package/.next/static/chunks/15qbli9udo0vh.js +0 -1
- package/.next/static/chunks/3p_pwu5gx2i9h.js +0 -1
- package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
- /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
- /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{YNRcnda8lvMnnrOKL1L5l → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
|
@@ -59,7 +59,7 @@ _ENV_LOCAL = _load_env_local()
|
|
|
59
59
|
# a server they don't control.
|
|
60
60
|
APP_URL = os.getenv("APP_URL", "http://localhost:3000")
|
|
61
61
|
MESSAGING_URL = "https://www.linkedin.com/messaging/"
|
|
62
|
-
MESSAGE_SCROLLS =
|
|
62
|
+
MESSAGE_SCROLLS = 15
|
|
63
63
|
|
|
64
64
|
CONNECTIONS_URL = "https://www.linkedin.com/mynetwork/invite-connect/connections/"
|
|
65
65
|
|
|
@@ -330,7 +330,7 @@ def ensure_logged_in(page, timeout_s=LOGIN_WAIT_SECONDS, log_fn=None, stop_on_ch
|
|
|
330
330
|
if stop_on_checkpoint and signed_out:
|
|
331
331
|
reason = "a security check" if wall == "checkpoint" else "being signed out"
|
|
332
332
|
_keep_pushback_evidence(page, reason)
|
|
333
|
-
set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
|
|
333
|
+
set_cooldown(seconds=AUTO_PUSHBACK_REST if EXPERIMENT["on"] else DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
|
|
334
334
|
raise LinkedInPushedBack(0, reason)
|
|
335
335
|
|
|
336
336
|
if _has_session_cookie(context) and not _looks_logged_out(page) and not wall:
|
|
@@ -1282,10 +1282,17 @@ LEGACY_PAGES_READ = 10 # how far every read before 0.1.6 went, at mos
|
|
|
1282
1282
|
# A sitting of SESSION_PAGES searches, then a long rest; searches only in the
|
|
1283
1283
|
# daytime on this computer's clock; and at the daily budget it waits for the
|
|
1284
1284
|
# budget to free up instead of stopping. Pages are saved one at a time.
|
|
1285
|
+
#
|
|
1286
|
+
# Its own ceilings sit under whatever budget is set on the Scan page: the account
|
|
1287
|
+
# restricted on 2026-09-28 did 373 searches in 24 hours with the budget raised to
|
|
1288
|
+
# 500. Auto scan stays far below that however high the budget is set.
|
|
1285
1289
|
EXPERIMENT = {"on": False, "pages": 0, "wire": 0}
|
|
1286
|
-
SESSION_PAGES =
|
|
1287
|
-
SESSION_REST =
|
|
1288
|
-
DRIP_HOURS = (9,
|
|
1290
|
+
SESSION_PAGES = 8 # searches in one sitting
|
|
1291
|
+
SESSION_REST = 60 * 60 # the rest after each sitting
|
|
1292
|
+
DRIP_HOURS = (9, 18) # searches only from 09:00 to 18:00, local time
|
|
1293
|
+
AUTO_DAY_CAP = 40 # at most this many searches in any 24 hours
|
|
1294
|
+
AUTO_WEEK_CAP = 200 # and this many in any 7 days
|
|
1295
|
+
AUTO_PUSHBACK_REST = 2 * 24 * 3600 # after any LinkedIn check, nothing for two days
|
|
1289
1296
|
WIRE_SAMPLES = 5 # raw LinkedIn responses kept per run, for research
|
|
1290
1297
|
|
|
1291
1298
|
|
|
@@ -1899,6 +1906,18 @@ def voyager_total(payload):
|
|
|
1899
1906
|
return None
|
|
1900
1907
|
|
|
1901
1908
|
|
|
1909
|
+
_WORDS = {"body", "text", "subject", "attributedBody", "snippet", "messages", "headline", "firstName", "lastName", "rootUrl", "artifacts", "sponsoredPreviewText", "customPronoun", "advertiserLabel"}
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def _redact_words(x):
|
|
1913
|
+
"""A copy of a LinkedIn response with every piece of writing and every name removed."""
|
|
1914
|
+
if isinstance(x, dict):
|
|
1915
|
+
return {k: ("<removed>" if k in _WORDS else _redact_words(v)) for k, v in x.items()}
|
|
1916
|
+
if isinstance(x, list):
|
|
1917
|
+
return [_redact_words(v) for v in x]
|
|
1918
|
+
return x
|
|
1919
|
+
|
|
1920
|
+
|
|
1902
1921
|
def _wire_sample(data, prefix="search"):
|
|
1903
1922
|
if EXPERIMENT["wire"] >= WIRE_SAMPLES:
|
|
1904
1923
|
return
|
|
@@ -1908,6 +1927,10 @@ def _wire_sample(data, prefix="search"):
|
|
|
1908
1927
|
return
|
|
1909
1928
|
folder = _home() / "wire-samples"
|
|
1910
1929
|
folder.mkdir(parents=True, exist_ok=True)
|
|
1930
|
+
if prefix == "messages":
|
|
1931
|
+
# Messaging responses carry what people wrote, and their names: a sample
|
|
1932
|
+
# keeps only the shape (keys, links, dates, counts), never the words.
|
|
1933
|
+
raw = json.dumps(_redact_words(data))
|
|
1911
1934
|
(folder / f"{prefix}-{int(time.time() * 1000)}.json").write_text(raw)
|
|
1912
1935
|
EXPERIMENT["wire"] += 1
|
|
1913
1936
|
except Exception:
|
|
@@ -1948,14 +1971,51 @@ def _wire_merge(page, profile_url, page_results):
|
|
|
1948
1971
|
return page_results
|
|
1949
1972
|
|
|
1950
1973
|
|
|
1951
|
-
_PROFILE_LINK = re.compile(r"https://www
|
|
1974
|
+
_PROFILE_LINK = re.compile(r"https://www\.linkedin\.com/in/[^/?#\s\"']+")
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
def _dig(x, *keys):
|
|
1978
|
+
"""x[k1][k2]..., or None as soon as a step isn't there."""
|
|
1979
|
+
for k in keys:
|
|
1980
|
+
if not isinstance(x, dict):
|
|
1981
|
+
return None
|
|
1982
|
+
x = x.get(k)
|
|
1983
|
+
return x
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
def _link_key(link):
|
|
1987
|
+
"""A profile link as the messages sync compares them: no query, one closing slash."""
|
|
1988
|
+
return link.split("?")[0].rstrip("/") + "/" if isinstance(link, str) and "/in/" in link else None
|
|
1952
1989
|
|
|
1953
1990
|
|
|
1954
1991
|
def voyager_conversations(payload):
|
|
1955
1992
|
"""Conversations in a LinkedIn messaging response (experimental): per conversation,
|
|
1956
|
-
everyone in it (profile links), when it was last active,
|
|
1957
|
-
|
|
1993
|
+
everyone in it (profile links), when it was last active, unread messages, its link,
|
|
1994
|
+
whether it's a group, and the newest message LinkedIn sent along with it (when,
|
|
1995
|
+
from whom, and its words). Anything with a lastActivityAt and participants,
|
|
1996
|
+
wherever it sits. The words are handed to the app only while Keep my messages is
|
|
1997
|
+
on there (sync_messages); a wire sample never has them (_redact_words)."""
|
|
1958
1998
|
out = []
|
|
1999
|
+
names = {}
|
|
2000
|
+
|
|
2001
|
+
def text_of(v):
|
|
2002
|
+
return v.get("text", "") if isinstance(v, dict) else (v if isinstance(v, str) else "")
|
|
2003
|
+
|
|
2004
|
+
def latest_of(x):
|
|
2005
|
+
# conversation.messages.elements[]: body.text, deliveredAt, and who sent it
|
|
2006
|
+
# (actor.participantType.member.profileUrl, the same /in/ACoA... link the
|
|
2007
|
+
# participants have, so it tells you from them).
|
|
2008
|
+
msgs = x.get("messages")
|
|
2009
|
+
elements = msgs.get("elements") if isinstance(msgs, dict) else msgs
|
|
2010
|
+
best = None
|
|
2011
|
+
for m in elements if isinstance(elements, list) else []:
|
|
2012
|
+
t = _dig(m, "deliveredAt")
|
|
2013
|
+
if not isinstance(t, (int, float)) or t < 1e12 or (best and t <= best["t"]):
|
|
2014
|
+
continue
|
|
2015
|
+
text = text_of(_dig(m, "body")).strip()
|
|
2016
|
+
best = {"t": int(t), "from": _link_key(_dig(m, "actor", "participantType", "member", "profileUrl")),
|
|
2017
|
+
"text": text or None}
|
|
2018
|
+
return best
|
|
1959
2019
|
|
|
1960
2020
|
def people(x):
|
|
1961
2021
|
found = set()
|
|
@@ -1966,6 +2026,14 @@ def voyager_conversations(payload):
|
|
|
1966
2026
|
pid = v.get("publicIdentifier")
|
|
1967
2027
|
if isinstance(pid, str) and pid and pid != "UNKNOWN":
|
|
1968
2028
|
found.add(f"https://www.linkedin.com/in/{pid}/")
|
|
2029
|
+
# Messaging gives a member's link as /in/ACoA… (their member id), not
|
|
2030
|
+
# the /in/name link your connections list has, so their name comes
|
|
2031
|
+
# along to match them up (the app keeps the match, not the name).
|
|
2032
|
+
key = _link_key(v.get("profileUrl"))
|
|
2033
|
+
if key:
|
|
2034
|
+
name = f"{text_of(v.get('firstName'))} {text_of(v.get('lastName'))}".strip()
|
|
2035
|
+
if name:
|
|
2036
|
+
names[key] = name
|
|
1969
2037
|
stack.extend(v.values())
|
|
1970
2038
|
elif isinstance(v, list):
|
|
1971
2039
|
stack.extend(v)
|
|
@@ -1984,7 +2052,13 @@ def voyager_conversations(payload):
|
|
|
1984
2052
|
who |= people(v)
|
|
1985
2053
|
if who:
|
|
1986
2054
|
unread = x.get("unreadCount")
|
|
1987
|
-
|
|
2055
|
+
link = x.get("conversationUrl")
|
|
2056
|
+
group = x.get("groupChat")
|
|
2057
|
+
out.append({"people": sorted(who), "at": int(at), "unread": int(unread) if isinstance(unread, (int, float)) else None,
|
|
2058
|
+
"names": {u: names[u] for u in who if u in names},
|
|
2059
|
+
"url": link if isinstance(link, str) and link.startswith("https://www.linkedin.com/messaging/") else None,
|
|
2060
|
+
"group": group if isinstance(group, bool) else None,
|
|
2061
|
+
"latest": latest_of(x)})
|
|
1988
2062
|
for v in x.values():
|
|
1989
2063
|
walk(v)
|
|
1990
2064
|
elif isinstance(x, list):
|
|
@@ -1997,8 +2071,9 @@ def voyager_conversations(payload):
|
|
|
1997
2071
|
def sync_messages(headless=False):
|
|
1998
2072
|
"""Experimental: read your LinkedIn messages list once, for the Social tab. Opens
|
|
1999
2073
|
Messaging in your own Chrome, scrolls the list a few times, and keeps from LinkedIn's
|
|
2000
|
-
own data
|
|
2001
|
-
|
|
2074
|
+
own data who each 1:1 conversation is with, when it was last active, whether it has
|
|
2075
|
+
unread messages, its link and who wrote last. The newest message's words go to the
|
|
2076
|
+
app only while Keep my messages is on there. No search budget."""
|
|
2002
2077
|
from playwright.sync_api import sync_playwright
|
|
2003
2078
|
kept = []
|
|
2004
2079
|
with sync_playwright() as p:
|
|
@@ -2021,7 +2096,12 @@ def sync_messages(headless=False):
|
|
|
2021
2096
|
if stop_requested() or _window_closed(page):
|
|
2022
2097
|
break
|
|
2023
2098
|
try:
|
|
2024
|
-
|
|
2099
|
+
# The list scrolls inside its own panel, not the window: find the panel
|
|
2100
|
+
# that holds the conversation links and scroll that to its end.
|
|
2101
|
+
page.evaluate("""() => {
|
|
2102
|
+
let l = document.querySelector('.msg-conversations-container__conversations-list, [class*="conversations-list"]');
|
|
2103
|
+
const a = document.querySelector('a[href*="/messaging/thread/"], li[class*="conversation-listitem"]');
|
|
2104
|
+
for (let e = a; !l && e; e = e.parentElement) if (e.scrollHeight > e.clientHeight + 20) l = e;
|
|
2025
2105
|
if (l) l.scrollTop = l.scrollHeight; }""")
|
|
2026
2106
|
except Exception:
|
|
2027
2107
|
pass
|
|
@@ -2036,7 +2116,35 @@ def sync_messages(headless=False):
|
|
|
2036
2116
|
_wire_sample(data, prefix="messages")
|
|
2037
2117
|
convos.extend(voyager_conversations(data))
|
|
2038
2118
|
browser.close()
|
|
2039
|
-
|
|
2119
|
+
_me, people = live_people(convos)
|
|
2120
|
+
print(f" Found {len(convos)} conversations in LinkedIn's data, {len(people)} with one person.")
|
|
2121
|
+
# The newest message's words go to the app only if Keep my messages is on
|
|
2122
|
+
# there. The app checks too; this way they don't even leave the scanner.
|
|
2123
|
+
keep = False
|
|
2124
|
+
try:
|
|
2125
|
+
r = requests.get(f"{APP_URL}/api/social", headers=app_headers(json_body=False), timeout=15)
|
|
2126
|
+
keep = r.ok and ((r.json() or {}).get("social") or {}).get("keepMessages") is True
|
|
2127
|
+
except Exception:
|
|
2128
|
+
pass
|
|
2129
|
+
if not keep:
|
|
2130
|
+
for v in people.values():
|
|
2131
|
+
if v.get("preview"):
|
|
2132
|
+
v["preview"]["text"] = None
|
|
2133
|
+
try:
|
|
2134
|
+
r = requests.put(f"{APP_URL}/api/social", json={"live": people}, headers=app_headers(), timeout=30)
|
|
2135
|
+
got = r.json() if r.ok else {}
|
|
2136
|
+
matched, unmatched = got.get("people", 0), got.get("unmatched", 0)
|
|
2137
|
+
print(f" Saved for the Social tab: {matched} matched to your connections, {unmatched} not.")
|
|
2138
|
+
except Exception as e:
|
|
2139
|
+
print(f" Couldn't save them: {e}")
|
|
2140
|
+
|
|
2141
|
+
|
|
2142
|
+
def live_people(convos):
|
|
2143
|
+
"""The messages sync's conversations, per person: you are the one in the most
|
|
2144
|
+
conversations, and only the 1:1 ones are kept. For each person, their newest
|
|
2145
|
+
conversation's last activity, unread count, name (for the app to match them to
|
|
2146
|
+
a connection; it keeps the match, not the name), link, who wrote last and the
|
|
2147
|
+
newest message. Returns (you, {profile link: ...})."""
|
|
2040
2148
|
counts = {}
|
|
2041
2149
|
for c in convos:
|
|
2042
2150
|
for u in c["people"]:
|
|
@@ -2045,17 +2153,23 @@ def sync_messages(headless=False):
|
|
|
2045
2153
|
people = {}
|
|
2046
2154
|
for c in convos:
|
|
2047
2155
|
others = [u for u in c["people"] if u != me]
|
|
2048
|
-
if len(others) != 1:
|
|
2156
|
+
if len(others) != 1 or c.get("group") is True:
|
|
2049
2157
|
continue
|
|
2050
2158
|
cur = people.get(others[0])
|
|
2051
|
-
if
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2159
|
+
if cur and c["at"] <= cur["last"]:
|
|
2160
|
+
continue
|
|
2161
|
+
entry = {"last": c["at"], "unread": c["unread"], "name": c.get("names", {}).get(others[0]),
|
|
2162
|
+
"threadUrl": c.get("url"), "lastFromThem": None, "preview": None}
|
|
2163
|
+
latest = c.get("latest")
|
|
2164
|
+
# Who sent it is known only when their link is one of the two in the
|
|
2165
|
+
# conversation; a link in some other form says nothing either way.
|
|
2166
|
+
sender = latest.get("from") if latest else None
|
|
2167
|
+
if sender in (me, others[0]):
|
|
2168
|
+
from_me = sender == me
|
|
2169
|
+
entry["lastFromThem"] = not from_me
|
|
2170
|
+
entry["preview"] = {"t": latest["t"], "fromMe": from_me, "text": latest.get("text")}
|
|
2171
|
+
people[others[0]] = entry
|
|
2172
|
+
return me, people
|
|
2059
2173
|
|
|
2060
2174
|
|
|
2061
2175
|
def _drip_wait(label, seconds):
|
|
@@ -2095,9 +2209,28 @@ def _drip_before_search():
|
|
|
2095
2209
|
if not _drip_wait("Today's searches are used", _seconds_until_search_frees()):
|
|
2096
2210
|
return False
|
|
2097
2211
|
continue
|
|
2212
|
+
wait = _auto_ceiling_wait()
|
|
2213
|
+
if wait:
|
|
2214
|
+
label, seconds = wait
|
|
2215
|
+
if not _drip_wait(label, seconds):
|
|
2216
|
+
return False
|
|
2217
|
+
continue
|
|
2098
2218
|
return True
|
|
2099
2219
|
|
|
2100
2220
|
|
|
2221
|
+
def _auto_ceiling_wait(now=None):
|
|
2222
|
+
"""Auto scan's own ceilings (AUTO_DAY_CAP in 24 hours, AUTO_WEEK_CAP in 7 days):
|
|
2223
|
+
None to go ahead, or (why, seconds until the oldest counted search drops out)."""
|
|
2224
|
+
now = now if now is not None else time.time()
|
|
2225
|
+
searches = sorted(t for t in _read_activity()["searches"] if now - 7 * DAY_SECONDS < t <= now)
|
|
2226
|
+
day = [t for t in searches if now - DAY_SECONDS < t]
|
|
2227
|
+
if len(day) >= AUTO_DAY_CAP:
|
|
2228
|
+
return (f"Auto scan's {AUTO_DAY_CAP} searches for today are used", day[-AUTO_DAY_CAP] + DAY_SECONDS - now + 5)
|
|
2229
|
+
if len(searches) >= AUTO_WEEK_CAP:
|
|
2230
|
+
return (f"Auto scan's {AUTO_WEEK_CAP} searches for this week are used", searches[-AUTO_WEEK_CAP] + 7 * DAY_SECONDS - now + 5)
|
|
2231
|
+
return None
|
|
2232
|
+
|
|
2233
|
+
|
|
2101
2234
|
def interruptible_sleep(seconds, on_tick=None, step=5):
|
|
2102
2235
|
"""Sleep, but notice a stop request. Returns False if we were interrupted."""
|
|
2103
2236
|
waited = 0
|
|
@@ -4073,7 +4206,7 @@ Examples:
|
|
|
4073
4206
|
parser.add_argument("--rescrape", type=str, help="Delete + re-scrape a bridge's cluster from scratch")
|
|
4074
4207
|
parser.add_argument("--company", type=str, help="Scan everyone the app can see at one company")
|
|
4075
4208
|
parser.add_argument("--messages", action="store_true",
|
|
4076
|
-
help="Experimental: read your messages list once for the Social tab (who, when, unread;
|
|
4209
|
+
help="Experimental: read your messages list once for the Social tab (who, when, unread; the newest message's words only if Keep my messages is on)")
|
|
4077
4210
|
parser.add_argument("--experimental", action="store_true",
|
|
4078
4211
|
help="Auto-Bridge, experimental: all-day pacing (sittings, rests, daytime only, "
|
|
4079
4212
|
"waits for the daily budget) and reading LinkedIn's own data beside the page text")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// A small Markdown renderer for the website's blog posts, pages and release notes.
|
|
2
|
+
// No dependencies, on purpose: the site builds with nothing but Node. It covers
|
|
3
|
+
// what the posts and CHANGELOG.md use, and nothing more:
|
|
4
|
+
//
|
|
5
|
+
// # to ####### headings (with an id from their text), paragraphs, blank-line breaks
|
|
6
|
+
// - and 1. lists, nested by two spaces, with continuation lines
|
|
7
|
+
// > block quotes, ``` fenced code, --- rules, | tables |
|
|
8
|
+
// **bold**, *italic*, `code`, [links](url), 
|
|
9
|
+
//
|
|
10
|
+
// Raw HTML is escaped, never passed through: a post can't inject markup or script.
|
|
11
|
+
|
|
12
|
+
export const escapeHtml = (s) => String(s)
|
|
13
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
14
|
+
|
|
15
|
+
/** "What it does, for you!" → "what-it-does-for-you" */
|
|
16
|
+
export function slugify(text) {
|
|
17
|
+
return String(text).toLowerCase()
|
|
18
|
+
.replace(/<[^>]+>/g, '')
|
|
19
|
+
.replace(/&[a-z]+;/g, '')
|
|
20
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
21
|
+
.trim().replace(/\s+/g, '-').replace(/-+/g, '-');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const safeUrl = (url) => (/^(https?:|mailto:|\/|#|\.\.?\/)/i.test(url) || !/^[a-z]+:/i.test(url) ? url : '#');
|
|
25
|
+
|
|
26
|
+
/** Inline marks, on text that is escaped first. */
|
|
27
|
+
export function inline(md) {
|
|
28
|
+
let s = escapeHtml(md);
|
|
29
|
+
const codes = [];
|
|
30
|
+
s = s.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
|
|
31
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^&]*)")?\)/g,
|
|
32
|
+
(_, alt, src) => `<img src="${safeUrl(src)}" alt="${alt}" loading="lazy">`);
|
|
33
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text, url) => `<a href="${safeUrl(url)}">${text}</a>`);
|
|
34
|
+
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
35
|
+
s = s.replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, '$1<em>$2</em>');
|
|
36
|
+
s = s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${codes[Number(i)]}</code>`);
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Front matter between --- lines: `key: value`, with [a, b] lists. Returns
|
|
42
|
+
* { data, body }. A file without it is all body.
|
|
43
|
+
*/
|
|
44
|
+
export function frontMatter(text) {
|
|
45
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
46
|
+
if (!m) return { data: {}, body: text };
|
|
47
|
+
const data = {};
|
|
48
|
+
for (const line of m[1].split('\n')) {
|
|
49
|
+
const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
|
|
50
|
+
if (!kv) continue;
|
|
51
|
+
let v = kv[2].trim();
|
|
52
|
+
if (/^\[.*\]$/.test(v)) v = v.slice(1, -1).split(',').map((x) => x.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
53
|
+
else v = v.replace(/^["'](.*)["']$/, '$1');
|
|
54
|
+
data[kv[1]] = v;
|
|
55
|
+
}
|
|
56
|
+
return { data, body: text.slice(m[0].length) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The same, for an HTML page whose first thing is a <!-- key: value --> comment. */
|
|
60
|
+
export function htmlFrontMatter(text) {
|
|
61
|
+
const m = text.match(/^<!--\n([\s\S]*?)\n-->\n?/);
|
|
62
|
+
if (!m) return { data: {}, body: text };
|
|
63
|
+
return { data: frontMatter(`---\n${m[1]}\n---\n`).data, body: text.slice(m[0].length) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function table(lines) {
|
|
67
|
+
const cells = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
68
|
+
const head = cells(lines[0]);
|
|
69
|
+
const rows = lines.slice(2).map(cells);
|
|
70
|
+
return `<div class="table-wrap prose-table"><table>\n<thead><tr>${head.map((h) => `<th scope="col">${inline(h)}</th>`).join('')}</tr></thead>\n<tbody>\n${
|
|
71
|
+
rows.map((r) => `<tr>${r.map((c, i) => (i === 0 ? `<th scope="row">${inline(c)}</th>` : `<td>${inline(c)}</td>`)).join('')}</tr>`).join('\n')
|
|
72
|
+
}\n</tbody></table></div>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Lists: items start with "- " or "1. " at an indent; deeper indents nest. An
|
|
77
|
+
* item's text, its sub-lists and any later paragraphs stay in their order.
|
|
78
|
+
*/
|
|
79
|
+
function list(lines, start) {
|
|
80
|
+
const indentOf = (l) => l.match(/^(\s*)/)[1].length;
|
|
81
|
+
const base = indentOf(lines[start]);
|
|
82
|
+
const ordered = /^\s*\d+\.\s/.test(lines[start]);
|
|
83
|
+
const items = [];
|
|
84
|
+
let i = start;
|
|
85
|
+
let gap = false; // a blank line: the next text inside this item is a new paragraph
|
|
86
|
+
while (i < lines.length) {
|
|
87
|
+
const l = lines[i];
|
|
88
|
+
if (!l.trim()) {
|
|
89
|
+
// A blank line inside a list continues it only if an indented line follows.
|
|
90
|
+
const next = lines[i + 1];
|
|
91
|
+
if (next && indentOf(next) > base && next.trim()) { gap = true; i += 1; continue; }
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const ind = indentOf(l);
|
|
95
|
+
const bullet = l.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/);
|
|
96
|
+
if (ind === base && bullet) { items.push({ parts: [bullet[1]] }); gap = false; i += 1; continue; }
|
|
97
|
+
if (ind < base || (!bullet && ind === base) || !items.length) break;
|
|
98
|
+
const parts = items.at(-1).parts;
|
|
99
|
+
if (bullet && ind > base) {
|
|
100
|
+
const [html, next] = list(lines, i);
|
|
101
|
+
parts.push({ html });
|
|
102
|
+
gap = false;
|
|
103
|
+
i = next;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (gap || typeof parts.at(-1) !== 'string') parts.push(l.trim());
|
|
107
|
+
else parts[parts.length - 1] += ` ${l.trim()}`;
|
|
108
|
+
gap = false;
|
|
109
|
+
i += 1;
|
|
110
|
+
}
|
|
111
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
112
|
+
const li = (it) => it.parts.map((p, n) => (typeof p !== 'string' ? p.html : n === 0 ? inline(p) : `<p>${inline(p)}</p>`)).join('');
|
|
113
|
+
const html = `<${tag}>\n${items.map((it) => `<li>${li(it)}</li>`).join('\n')}\n</${tag}>`;
|
|
114
|
+
return [html, i];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Markdown → HTML. `headingOffset` shifts heading levels (1 makes # an <h2>),
|
|
119
|
+
* `ids` adds an id to each heading.
|
|
120
|
+
*/
|
|
121
|
+
export function markdown(md, { headingOffset = 0, ids = true } = {}) {
|
|
122
|
+
const lines = md.replace(/\r\n/g, '\n').split('\n');
|
|
123
|
+
const out = [];
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < lines.length) {
|
|
126
|
+
const l = lines[i];
|
|
127
|
+
if (!l.trim()) { i += 1; continue; }
|
|
128
|
+
const fence = l.match(/^```(\w*)/);
|
|
129
|
+
if (fence) {
|
|
130
|
+
const code = [];
|
|
131
|
+
i += 1;
|
|
132
|
+
while (i < lines.length && !/^```/.test(lines[i])) code.push(lines[i++]);
|
|
133
|
+
i += 1;
|
|
134
|
+
out.push(`<pre><code${fence[1] ? ` class="lang-${fence[1]}"` : ''}>${escapeHtml(code.join('\n'))}</code></pre>`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const h = l.match(/^(#{1,6})\s+(.*)$/);
|
|
138
|
+
if (h) {
|
|
139
|
+
const level = Math.min(6, h[1].length + headingOffset);
|
|
140
|
+
const html = inline(h[2]);
|
|
141
|
+
out.push(`<h${level}${ids ? ` id="${slugify(h[2])}"` : ''}>${html}</h${level}>`);
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (/^(-{3,}|\*{3,})\s*$/.test(l)) { out.push('<hr>'); i += 1; continue; }
|
|
146
|
+
if (/^\s*(?:[-*]|\d+\.)\s+/.test(l)) {
|
|
147
|
+
const [html, next] = list(lines, i);
|
|
148
|
+
out.push(html);
|
|
149
|
+
i = next;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/^>\s?/.test(l)) {
|
|
153
|
+
const q = [];
|
|
154
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) q.push(lines[i++].replace(/^>\s?/, ''));
|
|
155
|
+
out.push(`<blockquote>${markdown(q.join('\n'), { headingOffset, ids: false })}</blockquote>`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (/^\|.*\|\s*$/.test(l) && /^\|?\s*:?-{2,}/.test(lines[i + 1] || '')) {
|
|
159
|
+
const t = [];
|
|
160
|
+
while (i < lines.length && /^\|.*\|\s*$/.test(lines[i])) t.push(lines[i++]);
|
|
161
|
+
out.push(table(t));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const img = l.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)\s*$/);
|
|
165
|
+
if (img) {
|
|
166
|
+
out.push(`<figure><img src="${escapeHtml(safeUrl(img[2]))}" alt="${escapeHtml(img[1])}" loading="lazy">${img[3] ? `<figcaption>${inline(img[3])}</figcaption>` : ''}</figure>`);
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const para = [];
|
|
171
|
+
while (i < lines.length && lines[i].trim() && !/^(#{1,6}\s|```|>\s?|\s*(?:[-*]|\d+\.)\s+|\|)/.test(lines[i])) para.push(lines[i++].trim());
|
|
172
|
+
if (!para.length) { para.push(lines[i++].trim()); }
|
|
173
|
+
out.push(`<p>${inline(para.join(' '))}</p>`);
|
|
174
|
+
}
|
|
175
|
+
return out.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Plain words from Markdown, for descriptions and feeds. */
|
|
179
|
+
export function plainText(md) {
|
|
180
|
+
return md
|
|
181
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
|
|
182
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
183
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
184
|
+
.replace(/(^|[^\w*])\*(?!\s)(.+?)\*(?!\w)/g, '$1$2')
|
|
185
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
186
|
+
.replace(/\s+/g, ' ')
|
|
187
|
+
.trim();
|
|
188
|
+
}
|