Skip to content

Commit b287d42

Browse files
committed
initial commit
1 parent ff76645 commit b287d42

File tree

21 files changed

+1583
-10
lines changed

21 files changed

+1583
-10
lines changed

.github/workflows/node.js.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
node-version: 24.x
1818
cache: 'npm'
1919
- run: npm ci
20+
- run: node ./generate-rulesets.ts
21+
- run: node ./generate-data-providers.ts
22+
- run: ./node_modules/.bin/eslint --fix ./generated-src/*.ts ./tests/generated/*.ts
2023
- run: ./node_modules/.bin/tsc --project ./tsconfig.app-check.json
2124
- run: ./node_modules/.bin/prettier . --check
2225
- run: ./node_modules/.bin/eslint --config eslint.config.js.mjs './**/*.mjs'
@@ -33,6 +36,9 @@ jobs:
3336
node-version: 24.x
3437
cache: 'npm'
3538
- run: npm ci
39+
- run: node ./generate-rulesets.ts
40+
- run: node ./generate-data-providers.ts
41+
- run: ./node_modules/.bin/eslint --fix ./generated-src/*.ts ./tests/generated/*.ts
3642
- name: Generate coverage
3743
run: ./node_modules/.bin/c8 --config=./.c8rc.coveralls.json node ./tests.ts
3844
- name: Coveralls GitHub Action

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
/**/*.map
55
/.eslintcache
66
/coverage/
7+
/generated-src/
8+
/tests/generated/

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "slugify"]
2+
path = slugify
3+
url = https://github.com/cocur/slugify.git

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
install:
22
@npm install
33

4-
build:
4+
generate:
5+
@echo 'generating rulesets'
6+
@node ./generate-rulesets.ts
7+
@echo 'generating data providers'
8+
@node ./generate-data-providers.ts
9+
@echo 'applying eslint to generated files'
10+
@./node_modules/.bin/eslint --fix ./generated-src/*.ts ./tests/generated/*.ts
11+
12+
build: generate
513
@echo 'building from ./tsconfig.app.json'
614
@./node_modules/.bin/tsc --project ./tsconfig.app.json
715

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Slugify
2+
3+
A partially-manual, partially-code-generated port of [cocur/slugify](https://github.com/cocur/slugify)
4+
5+
## Why?
6+
7+
In the process of rewriting the site builder for the
8+
[Satisfactory Clips Archive](https://archive.satisfactory.video/)
9+
it became desirable to have a slugifier that was compatible with the one
10+
used by the current site builder- `cocur/slugify`.
11+
12+
## Partially-code-generated?
13+
14+
Using a combination of TypeScript's factory methods and
15+
[php-parser](https://github.com/glayzzle/php-parser) some essential types
16+
are transposed to TypeScript at build time to ensure a reasonable degree
17+
of compatibility with `cocur/slugify`.
18+
19+
### Do you mean vibe-coded?
20+
21+
AHAHAHAHAHAHAHAHAHAHAHAHAHA
22+
23+
No.
24+
25+
## Partially-manual?
26+
27+
While one could _hypothetically_ perform code generation to port the PHP
28+
source to TypeScript, it's more straightforward to hand-write the
29+
core implementation.
30+
31+
[`strtr()`](https://locutus.io/php/strings/strtr/) is pulled in as a
32+
dependency from [locutus](https://github.com/locutusjs/locutus/), as while
33+
a dependency-free implementation was attempted, it had compatibility
34+
issues with `cocur/slugify`.
35+
36+
## Compatibility with cocur/slugify
37+
38+
- Only the rules/rulesets methods/options are implemented.
39+
- The default rulesets are generated at build time using TypeScript's factory methods.
40+
- Tests for testing `\\Cocur\\Slugify::slugify()` will be partially generated at build time.

generate-data-providers.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
readFile,
3+
} from 'node:fs/promises';
4+
5+
import {
6+
createWriteStream,
7+
} from 'node:fs';
8+
9+
import {
10+
Engine,
11+
} from 'php-parser';
12+
13+
import {
14+
createPrinter,
15+
createSourceFile,
16+
EmitHint,
17+
factory,
18+
NewLineKind,
19+
NodeFlags,
20+
ScriptKind,
21+
ScriptTarget,
22+
SyntaxKind,
23+
} from 'typescript';
24+
25+
import Array from 'php-parser/src/ast/array.js';
26+
import Call from 'php-parser/src/ast/call.js';
27+
import type Declaration from 'php-parser/src/ast/declaration.js';
28+
import Entry from 'php-parser/src/ast/entry.js';
29+
import Method from 'php-parser/src/ast/method.js';
30+
import Return from 'php-parser/src/ast/return.js';
31+
import String from 'php-parser/src/ast/string.js';
32+
33+
import type {
34+
str_repeat,
35+
substr,
36+
} from './utilities/php-parser.ts';
37+
import {
38+
find_in_class_body,
39+
is_array,
40+
is_str_repeat,
41+
is_substr,
42+
make_str_repeat_call,
43+
make_substr_call,
44+
name_matches,
45+
} from './utilities/php-parser.ts';
46+
47+
const parser = new Engine({});
48+
49+
const SlugifyTest_php_filepath = `${
50+
import.meta.dirname
51+
}/slugify/tests/SlugifyTest.php`;
52+
53+
const ast = parser.parseCode(
54+
(await readFile(SlugifyTest_php_filepath)).toString(),
55+
SlugifyTest_php_filepath,
56+
);
57+
58+
const defaultRuleProvider = find_in_class_body(
59+
ast,
60+
'Cocur\\Slugify\\Tests',
61+
'SlugifyTest',
62+
(
63+
maybe: Declaration<string, string>,
64+
): maybe is Method<'defaultRuleProvider'> => (
65+
maybe instanceof Method
66+
&& name_matches('defaultRuleProvider', maybe.name)
67+
),
68+
);
69+
70+
const defaultRuleProviderReturn = defaultRuleProvider.body?.children.find(
71+
(maybe): maybe is Return => maybe instanceof Return,
72+
);
73+
74+
if (!defaultRuleProviderReturn) {
75+
throw new Error('Could not find return statement!');
76+
} else if (!(defaultRuleProviderReturn.expr instanceof Array)) {
77+
throw new Error('Return statement was not an array!');
78+
} else if ((defaultRuleProviderReturn.expr as Array).items.find((maybe) => !(
79+
maybe instanceof Entry
80+
&& is_array<[
81+
Entry<null, (
82+
| String
83+
| str_repeat
84+
| substr
85+
)>,
86+
Entry<null, (
87+
| String
88+
| str_repeat
89+
| substr
90+
)>,
91+
]>(
92+
maybe.value,
93+
2,
94+
(item) => (
95+
item instanceof Entry
96+
&& null === item.key
97+
&& (
98+
item.value instanceof String
99+
|| (
100+
item.value instanceof Call
101+
&& (
102+
is_str_repeat(item.value)
103+
|| is_substr(item.value)
104+
)
105+
)
106+
)
107+
),
108+
)
109+
))) {
110+
throw new Error('Unexpected value found on return expression!');
111+
}
112+
113+
const coerced = (
114+
defaultRuleProviderReturn.expr.items as Entry<
115+
null,
116+
Array<[
117+
Entry<null, (
118+
| String
119+
| str_repeat
120+
| substr
121+
)>,
122+
Entry<null, (
123+
| String
124+
| str_repeat
125+
| substr
126+
)>,
127+
]>
128+
>[]
129+
);
130+
131+
const mapped = coerced.map((node) => node.value.items.map((item): string => {
132+
if (item.value instanceof String) {
133+
return item.value.value;
134+
} else if (is_str_repeat(item.value)) {
135+
return make_str_repeat_call(item.value);
136+
}
137+
138+
return make_substr_call(item.value);
139+
}) as [string, string]);
140+
141+
const defaultRuleProvider_export = factory.createVariableStatement(
142+
[
143+
factory.createToken(SyntaxKind.ExportKeyword),
144+
],
145+
factory.createVariableDeclarationList(
146+
[
147+
factory.createVariableDeclaration(
148+
'defaultRuleProvider',
149+
undefined,
150+
undefined,
151+
factory.createArrayLiteralExpression(
152+
mapped.map((row) => factory.createArrayLiteralExpression(
153+
row.map((value) => factory.createStringLiteral(value)),
154+
true,
155+
)),
156+
true,
157+
),
158+
),
159+
],
160+
NodeFlags.Const,
161+
),
162+
);
163+
164+
165+
const printer = createPrinter({
166+
newLine: NewLineKind.LineFeed,
167+
noEmitHelpers: true,
168+
});
169+
170+
const filepath = `${
171+
import.meta.dirname
172+
}/tests/generated/defaultRuleProvider.ts`;
173+
174+
const source_file = createSourceFile(
175+
SlugifyTest_php_filepath,
176+
'',
177+
ScriptTarget.Latest,
178+
false,
179+
ScriptKind.TS,
180+
);
181+
182+
const stream = createWriteStream(filepath);
183+
184+
stream.write('/* eslint-disable @stylistic/max-len */');
185+
stream.write('\n\n');
186+
stream.write(printer.printNode(
187+
EmitHint.Unspecified,
188+
defaultRuleProvider_export,
189+
source_file,
190+
));
191+
192+
await new Promise<void>((yup) => {
193+
stream.end(() => {
194+
yup();
195+
});
196+
});

0 commit comments

Comments
 (0)