Skip to content

Commit 92fc683

Browse files
committed
Merge branch 'main' into @ksienkiewicz/feat-max-length
2 parents 4e6f4be + c070294 commit 92fc683

23 files changed

Lines changed: 521 additions & 48 deletions

.github/workflows/docs-build.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,17 @@ jobs:
3939

4040
- name: Build docs
4141
run: yarn build
42+
43+
- name: Check docs llms.txt
44+
run: |
45+
file=build/llms.txt
46+
if [ ! -s "$file" ]; then
47+
echo "::error::$file is missing or empty"
48+
exit 1
49+
fi
50+
count=$(grep -cE '^- \[[^]]+\]\(https://docs\.swmansion\.com/react-native-enriched-html/[^)]+\)' "$file" || true)
51+
if [ "$count" -eq 0 ]; then
52+
echo "::error::$file lists no pages"
53+
exit 1
54+
fi
55+
echo "llms.txt lists $count pages"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
appId: swmansion.enriched.example
2+
---
3+
# Verifies that typing attributes are properly preserved on selection changes
4+
- launchApp
5+
6+
- tapOn:
7+
id: 'toggle-screen-button'
8+
9+
- tapOn:
10+
id: "editor-input"
11+
12+
- tapOn:
13+
id: "toolbar-bold"
14+
15+
- inputText: 'bold text'
16+
- pressKey: Enter
17+
- pressKey: Enter
18+
- pressKey: Enter
19+
- inputText: 'another line'
20+
21+
- doubleTapOn:
22+
id: 'editor-input'
23+
point: '20%, 75%'
24+
25+
- tapOn:
26+
id: 'editor-input'
27+
point: '50%, 15%'
28+
29+
- inputText: 'new'
30+
31+
- runFlow:
32+
file: '../subflows/capture_or_assert_screenshot.yaml'
33+
env:
34+
SCREENSHOT_NAME: 'preserve_typing_attributes_on_selection_changes'
9.81 KB
Loading
8 Bytes
Loading
21 Bytes
Loading
11.6 KB
Loading

docs/docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ const config = {
134134
],
135135

136136
plugins: [
137+
require('./plugins/swm-geo'),
137138
reactNativeWebPlugin,
138139
enrichedHtmlLocalSourcePlugin,
139140
function transpileTRexUiTheme() {

docs/plugins/swm-geo.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
4+
const ORGANIZATION_ID = 'https://swmansion.com/#organization';
5+
const SECTION_NAMES = { docs: 'Documentation' };
6+
const ACRONYMS = { api: 'API', ui: 'UI' };
7+
8+
const titleCase = (segment) =>
9+
segment
10+
.split(/[-_]/)
11+
.map((word) => ACRONYMS[word] ?? word.replace(/^./, (c) => c.toUpperCase()))
12+
.join(' ');
13+
14+
const sectionOf = (relative) => {
15+
const parts = relative.split('/').filter(Boolean);
16+
if (SECTION_NAMES[parts[0]]) return SECTION_NAMES[parts[0]];
17+
if (parts.length < 2) return 'Pages';
18+
return titleCase(parts[0]);
19+
};
20+
21+
const decode = (value) =>
22+
value
23+
.replace(/&amp;/g, '&')
24+
.replace(/&lt;/g, '<')
25+
.replace(/&gt;/g, '>')
26+
.replace(/&quot;/g, '"')
27+
.replace(/&#(?:39|x27);/g, "'")
28+
.trim();
29+
30+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31+
32+
const metaOf = (html, name) =>
33+
new RegExp(
34+
`<meta[^>]+name="${escapeRegExp(name)}"[^>]+content="([^"]*)"`,
35+
'i',
36+
).exec(html)?.[1] ?? '';
37+
38+
function describe(html, siteTitle) {
39+
const raw = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1] ?? '';
40+
const title = decode(raw).replace(
41+
new RegExp(`\\s*\\|\\s*${escapeRegExp(siteTitle)}$`),
42+
'',
43+
);
44+
return { title, description: decode(metaOf(html, 'description')) };
45+
}
46+
47+
// Same @id as swmansion.com, so engines read one company across both domains.
48+
function buildStructuredData(siteConfig) {
49+
const { organizationName, projectName, tagline, title } = siteConfig;
50+
const repository =
51+
organizationName && projectName
52+
? `https://github.com/${organizationName}/${projectName}`
53+
: undefined;
54+
55+
return {
56+
'@context': 'https://schema.org',
57+
'@graph': [
58+
{
59+
'@type': 'Organization',
60+
'@id': ORGANIZATION_ID,
61+
name: 'Software Mansion',
62+
url: 'https://swmansion.com',
63+
sameAs: [
64+
'https://github.com/software-mansion',
65+
'https://www.linkedin.com/company/software-mansion/',
66+
'https://twitter.com/swmansion',
67+
'https://www.youtube.com/c/SoftwareMansion',
68+
],
69+
},
70+
{
71+
'@type': 'SoftwareSourceCode',
72+
name: title,
73+
...(tagline ? { description: tagline } : {}),
74+
...(repository ? { codeRepository: repository } : {}),
75+
author: { '@id': ORGANIZATION_ID },
76+
maintainer: { '@id': ORGANIZATION_ID },
77+
},
78+
],
79+
};
80+
}
81+
82+
function buildLlmsTxt({ siteConfig, routesPaths, readPage }) {
83+
const { baseUrl, title, tagline, url } = siteConfig;
84+
const grouped = new Map();
85+
86+
for (const route of routesPaths) {
87+
if (!route.startsWith(baseUrl) || route.endsWith('404.html')) continue;
88+
89+
const relative = route.slice(baseUrl.length);
90+
const html = readPage(relative);
91+
if (!html) continue;
92+
93+
const page = describe(html, title);
94+
if (!page.title) continue;
95+
96+
const section = sectionOf(relative);
97+
const line = `- [${page.title}](${url.replace(/\/$/, '')}${route})${page.description ? `: ${page.description}` : ''}`;
98+
99+
if (!grouped.has(section)) grouped.set(section, []);
100+
grouped.get(section).push(line);
101+
}
102+
103+
const lines = [`# ${title}`];
104+
if (tagline) lines.push('', `> ${tagline}`);
105+
106+
// Documentation first, the rest alphabetically, loose pages last.
107+
const order = [
108+
...(grouped.has('Documentation') ? ['Documentation'] : []),
109+
...[...grouped.keys()]
110+
.filter((section) => section !== 'Documentation' && section !== 'Pages')
111+
.sort(),
112+
...(grouped.has('Pages') ? ['Pages'] : []),
113+
];
114+
115+
for (const section of order) {
116+
const entries = grouped.get(section);
117+
if (!entries?.length) continue;
118+
lines.push('', `## ${section}`, '', ...entries.sort());
119+
}
120+
121+
lines.push(
122+
'',
123+
'## About',
124+
'',
125+
`- [Software Mansion](https://swmansion.com): maintainer of ${title}`,
126+
'',
127+
);
128+
129+
return lines.join('\n');
130+
}
131+
132+
module.exports = function swmGeoPlugin(context) {
133+
return {
134+
name: 'swm-geo',
135+
136+
injectHtmlTags() {
137+
return {
138+
headTags: [
139+
{
140+
tagName: 'script',
141+
attributes: { type: 'application/ld+json' },
142+
innerHTML: JSON.stringify(
143+
buildStructuredData(context.siteConfig),
144+
).replace(/</g, '\\u003c'),
145+
},
146+
],
147+
};
148+
},
149+
150+
async postBuild({ siteConfig, routesPaths, outDir }) {
151+
const readPage = (relative) => {
152+
const file = path.join(outDir, relative, 'index.html');
153+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
154+
};
155+
156+
await fs.promises.writeFile(
157+
path.join(outDir, 'llms.txt'),
158+
buildLlmsTxt({ siteConfig, routesPaths, readPage }),
159+
'utf8',
160+
);
161+
},
162+
};
163+
};
164+
165+
module.exports.buildLlmsTxt = buildLlmsTxt;
166+
module.exports.buildStructuredData = buildStructuredData;

docs/static/robots.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.

ios/EnrichedTextInputView.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,14 @@ - (bool)textView:(UITextView *)textView
20282028
return NO;
20292029
}
20302030

2031+
// To be sure, we re-run typingAttributes management right before the
2032+
// character actually lands. Sometimes, between a selection change and the
2033+
// next keystroke, typing attributes might get removed - this seems like a
2034+
// native TextKit issue.
2035+
if (textView.markedTextRange == nil && text.length > 0) {
2036+
[attributesManager repeatRecentTypingAttributesManagement];
2037+
}
2038+
20312039
// maxLength has to be checked as the very last thing - the handlers above
20322040
// manage the text on their own and none of them makes it any longer
20332041
if ([self handleMaxLengthInRange:range replacementText:text]) {

0 commit comments

Comments
 (0)