@qrxcode/js 0.1.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.
- package/README.md +113 -0
- package/dist/index.cjs +93 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +64 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# QRX flow discovery SDK
|
|
2
|
+
|
|
3
|
+
Detect RSS, Atom and JSON Feed flows from normal website and page URLs.
|
|
4
|
+
|
|
5
|
+
With QRX, applications with QR scanners can interact with QR codes
|
|
6
|
+
more like browsers interact with links.
|
|
7
|
+
|
|
8
|
+
QRX works with:
|
|
9
|
+
|
|
10
|
+
- QR code scanners
|
|
11
|
+
- pasted links
|
|
12
|
+
- shared URLs
|
|
13
|
+
- typed URLs
|
|
14
|
+
- browser extensions
|
|
15
|
+
- app share sheets
|
|
16
|
+
- any workflow that receives a URL
|
|
17
|
+
|
|
18
|
+
A normal QR code still contains a normal URL.
|
|
19
|
+
|
|
20
|
+
QRX-compatible applications resolve the URL,
|
|
21
|
+
discover machine-readable flows from the HTML `<head>`,
|
|
22
|
+
and let the application decide what to do next.
|
|
23
|
+
|
|
24
|
+
This works especially well with podcast websites,
|
|
25
|
+
because many podcast sites already expose one or multiple feeds.
|
|
26
|
+
|
|
27
|
+
With QRX, podcast subscriptions can become as simple
|
|
28
|
+
and natural as following someone on social media.
|
|
29
|
+
|
|
30
|
+
Examples of flows:
|
|
31
|
+
|
|
32
|
+
- RSS feeds
|
|
33
|
+
- Atom feeds
|
|
34
|
+
- JSON feeds
|
|
35
|
+
|
|
36
|
+
At the moment, "flow" is a broad term used by QRX
|
|
37
|
+
to describe machine-readable update streams and feeds.
|
|
38
|
+
|
|
39
|
+
QRX does not replace RSS or existing feed technologies.
|
|
40
|
+
|
|
41
|
+
QRX helps applications discover and work with them more naturally.
|
|
42
|
+
|
|
43
|
+
Learn more at https://qrx.dev
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @qrxcode/js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { resolveQRX } from "@qrxcode/js";
|
|
55
|
+
|
|
56
|
+
const result = await resolveQRX(
|
|
57
|
+
"https://podnews.net"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
console.log(result.flows);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Example output
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
type: "rss",
|
|
69
|
+
url: "https://podnews.net/rss"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "jsonfeed",
|
|
73
|
+
url: "https://podnews.net/feed.json"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Supported flow types
|
|
79
|
+
|
|
80
|
+
* RSS
|
|
81
|
+
* Atom
|
|
82
|
+
* JSON Feed
|
|
83
|
+
|
|
84
|
+
## Supported discovery methods
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<link
|
|
88
|
+
rel="alternate"
|
|
89
|
+
type="application/rss+xml"
|
|
90
|
+
href="/feed.xml">
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<link
|
|
95
|
+
rel="alternate"
|
|
96
|
+
type="application/atom+xml"
|
|
97
|
+
href="/atom.xml">
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<link
|
|
102
|
+
rel="alternate"
|
|
103
|
+
type="application/feed+json"
|
|
104
|
+
href="/feed.json">
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Philosophy
|
|
108
|
+
|
|
109
|
+
QRX does not change QR codes.
|
|
110
|
+
|
|
111
|
+
With QRX, sources can expose machine-readable flows,
|
|
112
|
+
and applications can understand and interact with them naturally.
|
|
113
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
detectFlows: () => detectFlows,
|
|
24
|
+
resolveQRX: () => resolveQRX,
|
|
25
|
+
selectFlows: () => selectFlows
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/detect.ts
|
|
30
|
+
var FLOW_TYPES = {
|
|
31
|
+
"application/rss+xml": "rss",
|
|
32
|
+
"application/atom+xml": "atom",
|
|
33
|
+
"application/feed+json": "jsonfeed"
|
|
34
|
+
};
|
|
35
|
+
function detectFlows(html, sourceUrl) {
|
|
36
|
+
const flows = [];
|
|
37
|
+
const linkTagRegex = /<link\s+[^>]*>/gi;
|
|
38
|
+
const tags = html.match(linkTagRegex) || [];
|
|
39
|
+
for (const tag of tags) {
|
|
40
|
+
const rel = getAttribute(tag, "rel");
|
|
41
|
+
const type = getAttribute(tag, "type");
|
|
42
|
+
const href = getAttribute(tag, "href");
|
|
43
|
+
if (!rel || !type || !href) continue;
|
|
44
|
+
if (!rel.includes("alternate")) continue;
|
|
45
|
+
const flowType = FLOW_TYPES[type];
|
|
46
|
+
if (!flowType) continue;
|
|
47
|
+
flows.push({
|
|
48
|
+
type: flowType,
|
|
49
|
+
url: new URL(href, sourceUrl).toString()
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return dedupeFlows(flows);
|
|
53
|
+
}
|
|
54
|
+
function getAttribute(tag, attribute) {
|
|
55
|
+
const regex = new RegExp(
|
|
56
|
+
`${attribute}=["']([^"']+)["']`,
|
|
57
|
+
"i"
|
|
58
|
+
);
|
|
59
|
+
const match = tag.match(regex);
|
|
60
|
+
return match ? match[1] : null;
|
|
61
|
+
}
|
|
62
|
+
function dedupeFlows(flows) {
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
return flows.filter((flow) => {
|
|
65
|
+
const key = `${flow.type}:${flow.url}`;
|
|
66
|
+
if (seen.has(key)) return false;
|
|
67
|
+
seen.add(key);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/resolve.ts
|
|
73
|
+
async function resolveQRX(url) {
|
|
74
|
+
const response = await fetch(url);
|
|
75
|
+
const html = await response.text();
|
|
76
|
+
const flows = detectFlows(html, response.url);
|
|
77
|
+
return {
|
|
78
|
+
flows
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/select.ts
|
|
83
|
+
function selectFlows(flows, preferredTypes) {
|
|
84
|
+
return flows.filter(
|
|
85
|
+
(flow) => preferredTypes.includes(flow.type)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
89
|
+
0 && (module.exports = {
|
|
90
|
+
detectFlows,
|
|
91
|
+
resolveQRX,
|
|
92
|
+
selectFlows
|
|
93
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type FlowType = "rss" | "atom" | "jsonfeed";
|
|
2
|
+
interface Flow {
|
|
3
|
+
type: FlowType;
|
|
4
|
+
url: string;
|
|
5
|
+
}
|
|
6
|
+
interface QRXResult {
|
|
7
|
+
flows: Flow[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare function detectFlows(html: string, sourceUrl: string): Flow[];
|
|
11
|
+
|
|
12
|
+
declare function resolveQRX(url: string): Promise<QRXResult>;
|
|
13
|
+
|
|
14
|
+
declare function selectFlows(flows: Flow[], preferredTypes: FlowType[]): Flow[];
|
|
15
|
+
|
|
16
|
+
export { type Flow, type FlowType, type QRXResult, detectFlows, resolveQRX, selectFlows };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type FlowType = "rss" | "atom" | "jsonfeed";
|
|
2
|
+
interface Flow {
|
|
3
|
+
type: FlowType;
|
|
4
|
+
url: string;
|
|
5
|
+
}
|
|
6
|
+
interface QRXResult {
|
|
7
|
+
flows: Flow[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare function detectFlows(html: string, sourceUrl: string): Flow[];
|
|
11
|
+
|
|
12
|
+
declare function resolveQRX(url: string): Promise<QRXResult>;
|
|
13
|
+
|
|
14
|
+
declare function selectFlows(flows: Flow[], preferredTypes: FlowType[]): Flow[];
|
|
15
|
+
|
|
16
|
+
export { type Flow, type FlowType, type QRXResult, detectFlows, resolveQRX, selectFlows };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/detect.ts
|
|
2
|
+
var FLOW_TYPES = {
|
|
3
|
+
"application/rss+xml": "rss",
|
|
4
|
+
"application/atom+xml": "atom",
|
|
5
|
+
"application/feed+json": "jsonfeed"
|
|
6
|
+
};
|
|
7
|
+
function detectFlows(html, sourceUrl) {
|
|
8
|
+
const flows = [];
|
|
9
|
+
const linkTagRegex = /<link\s+[^>]*>/gi;
|
|
10
|
+
const tags = html.match(linkTagRegex) || [];
|
|
11
|
+
for (const tag of tags) {
|
|
12
|
+
const rel = getAttribute(tag, "rel");
|
|
13
|
+
const type = getAttribute(tag, "type");
|
|
14
|
+
const href = getAttribute(tag, "href");
|
|
15
|
+
if (!rel || !type || !href) continue;
|
|
16
|
+
if (!rel.includes("alternate")) continue;
|
|
17
|
+
const flowType = FLOW_TYPES[type];
|
|
18
|
+
if (!flowType) continue;
|
|
19
|
+
flows.push({
|
|
20
|
+
type: flowType,
|
|
21
|
+
url: new URL(href, sourceUrl).toString()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return dedupeFlows(flows);
|
|
25
|
+
}
|
|
26
|
+
function getAttribute(tag, attribute) {
|
|
27
|
+
const regex = new RegExp(
|
|
28
|
+
`${attribute}=["']([^"']+)["']`,
|
|
29
|
+
"i"
|
|
30
|
+
);
|
|
31
|
+
const match = tag.match(regex);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
function dedupeFlows(flows) {
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
return flows.filter((flow) => {
|
|
37
|
+
const key = `${flow.type}:${flow.url}`;
|
|
38
|
+
if (seen.has(key)) return false;
|
|
39
|
+
seen.add(key);
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/resolve.ts
|
|
45
|
+
async function resolveQRX(url) {
|
|
46
|
+
const response = await fetch(url);
|
|
47
|
+
const html = await response.text();
|
|
48
|
+
const flows = detectFlows(html, response.url);
|
|
49
|
+
return {
|
|
50
|
+
flows
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/select.ts
|
|
55
|
+
function selectFlows(flows, preferredTypes) {
|
|
56
|
+
return flows.filter(
|
|
57
|
+
(flow) => preferredTypes.includes(flow.type)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
detectFlows,
|
|
62
|
+
resolveQRX,
|
|
63
|
+
selectFlows
|
|
64
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qrxcode/js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "QRX flow discovery SDK: scan once, app understands.",
|
|
5
|
+
"homepage": "https://qrx.dev",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/qrxcode/qrx-js.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/qrxcode/qrx-js/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist", "README.md"],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"dev": "vitest",
|
|
29
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"qrx",
|
|
33
|
+
"qr",
|
|
34
|
+
"rss",
|
|
35
|
+
"atom",
|
|
36
|
+
"jsonfeed",
|
|
37
|
+
"feed",
|
|
38
|
+
"podcast",
|
|
39
|
+
"scanner",
|
|
40
|
+
"discovery",
|
|
41
|
+
"directflow"
|
|
42
|
+
],
|
|
43
|
+
"author": "Yeldar Kudaibergen",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "latest",
|
|
47
|
+
"tsup": "latest",
|
|
48
|
+
"typescript": "latest",
|
|
49
|
+
"vitest": "latest"
|
|
50
|
+
}
|
|
51
|
+
}
|