mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 17:14:04 +00:00
Compare commits
No commits in common. "1233ae52caa06f019dff3d2aafe9549da59c939b" and "7ac0fc1c91f7ac7c6ee131842fdfe870697eeea7" have entirely different histories.
1233ae52ca
...
7ac0fc1c91
11 changed files with 29 additions and 689 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
# TUT 2.0 - 下次继续从这里开始
|
# TUT 2.0 - 下次继续从这里开始
|
||||||
|
|
||||||
## 当前位置
|
## 当前位置
|
||||||
- **阶段**: Phase 9 - 性能优化和测试工具 (已完成!)
|
- **阶段**: Phase 7 - 历史记录持久化 (已完成!)
|
||||||
- **进度**: 图片缓存、测试工具、文档完善
|
- **进度**: 历史记录自动保存,支持 :history 命令查看
|
||||||
- **最后提交**: `feat: Add comprehensive testing tools and improve help`
|
- **最后提交**: `feat: Add persistent browsing history`
|
||||||
|
|
||||||
## 立即可做的事
|
## 立即可做的事
|
||||||
|
|
||||||
|
|
@ -19,40 +19,8 @@
|
||||||
|
|
||||||
历史记录存储在 `~/.config/tut/history.json`
|
历史记录存储在 `~/.config/tut/history.json`
|
||||||
|
|
||||||
### 3. 表单交互
|
|
||||||
- **i** - 聚焦到第一个表单字段
|
|
||||||
- **Tab** - 下一个表单字段
|
|
||||||
- **Shift+Tab** - 上一个表单字段
|
|
||||||
- **Enter** - 激活字段(文本输入/下拉选择/复选框)
|
|
||||||
- 在文本输入模式下:
|
|
||||||
- 输入文字实时更新
|
|
||||||
- **Enter** 或 **Esc** - 退出编辑模式
|
|
||||||
- 在下拉选择模式下:
|
|
||||||
- **j/k** 或 **↓/↑** - 导航选项
|
|
||||||
- **Enter** - 选择当前选项
|
|
||||||
- **Esc** - 取消选择
|
|
||||||
|
|
||||||
## 已完成的功能清单
|
## 已完成的功能清单
|
||||||
|
|
||||||
### Phase 9 - 性能优化和测试工具
|
|
||||||
- [x] 图片 LRU 缓存 (100张,10分钟过期)
|
|
||||||
- [x] 缓存命中统计显示
|
|
||||||
- [x] 交互式测试脚本 (test_browser.sh)
|
|
||||||
- [x] 完整测试指南 (TESTING.md)
|
|
||||||
- [x] 帮助文档更新(包含所有新功能)
|
|
||||||
- [x] 测试清单和成功标准
|
|
||||||
|
|
||||||
### Phase 8 - 表单交互增强
|
|
||||||
- [x] 文本输入框编辑
|
|
||||||
- [x] 实时文本编辑和预览
|
|
||||||
- [x] Tab/Shift+Tab 字段导航
|
|
||||||
- [x] 复选框切换
|
|
||||||
- [x] 下拉选择(SELECT/OPTION)
|
|
||||||
- [x] SELECT 选项解析和存储
|
|
||||||
- [x] j/k 导航选项
|
|
||||||
- [x] 状态栏显示 INSERT/SELECT 模式
|
|
||||||
- [x] 'i' 键聚焦首个表单字段
|
|
||||||
|
|
||||||
### Phase 7 - 历史记录持久化
|
### Phase 7 - 历史记录持久化
|
||||||
- [x] HistoryEntry 数据结构 (URL, 标题, 访问时间)
|
- [x] HistoryEntry 数据结构 (URL, 标题, 访问时间)
|
||||||
- [x] JSON 持久化存储 (~/.config/tut/history.json)
|
- [x] JSON 持久化存储 (~/.config/tut/history.json)
|
||||||
|
|
@ -174,9 +142,8 @@ cmake --build build
|
||||||
| j/k | 上下滚动 |
|
| j/k | 上下滚动 |
|
||||||
| Ctrl+d/u | 翻页 |
|
| Ctrl+d/u | 翻页 |
|
||||||
| gg/G | 顶部/底部 |
|
| gg/G | 顶部/底部 |
|
||||||
| Tab/Shift+Tab | 切换链接/表单字段 |
|
| Tab/Shift+Tab | 切换链接 |
|
||||||
| Enter | 跟随链接/激活字段 |
|
| Enter | 跟随链接 |
|
||||||
| i | 聚焦首个表单字段 |
|
|
||||||
| h/l | 后退/前进 |
|
| h/l | 后退/前进 |
|
||||||
| / | 搜索 |
|
| / | 搜索 |
|
||||||
| n/N | 下一个/上一个匹配 |
|
| n/N | 下一个/上一个匹配 |
|
||||||
|
|
@ -188,27 +155,18 @@ cmake --build build
|
||||||
| :history | 查看历史 |
|
| :history | 查看历史 |
|
||||||
| :q | 退出 |
|
| :q | 退出 |
|
||||||
| ? | 帮助 |
|
| ? | 帮助 |
|
||||||
| Esc | 取消加载/退出编辑 |
|
| Esc | 取消加载 |
|
||||||
|
|
||||||
**表单编辑模式** (INSERT):
|
|
||||||
- 输入字符 - 编辑文本
|
|
||||||
- Enter/Esc - 完成编辑
|
|
||||||
|
|
||||||
**下拉选择模式** (SELECT):
|
|
||||||
- j/k, ↓/↑ - 导航选项
|
|
||||||
- Enter - 选择选项
|
|
||||||
- Esc - 取消选择
|
|
||||||
|
|
||||||
## 下一步功能优先级
|
## 下一步功能优先级
|
||||||
|
|
||||||
1. **异步图片加载** - 图片也使用异步加载
|
1. **更多表单交互** - 文本输入编辑,下拉选择
|
||||||
2. **Cookie 支持** - 保存和发送 Cookie
|
2. **图片缓存** - 避免重复下载相同图片
|
||||||
3. **表单提交** - 实现 POST 表单提交
|
3. **异步图片加载** - 图片也使用异步加载
|
||||||
4. **更多HTML5支持** - 更完善的HTML渲染
|
4. **Cookie 支持** - 保存和发送 Cookie
|
||||||
|
|
||||||
## 恢复对话时说
|
## 恢复对话时说
|
||||||
|
|
||||||
> "continue"
|
> "继续TUT 2.0开发"
|
||||||
|
|
||||||
## Git 信息
|
## Git 信息
|
||||||
|
|
||||||
|
|
@ -224,24 +182,5 @@ cmake --build build
|
||||||
./build/tut
|
./build/tut
|
||||||
```
|
```
|
||||||
|
|
||||||
## 测试指南
|
|
||||||
|
|
||||||
查看 `TESTING.md` 获取完整测试指南,或运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./test_browser.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 浏览器特性总结
|
|
||||||
|
|
||||||
✓ **核心功能** - 异步HTTP加载、页面缓存、差分渲染
|
|
||||||
✓ **导航** - 滚动、链接、历史记录
|
|
||||||
✓ **搜索** - 全文搜索、高亮、导航
|
|
||||||
✓ **表单** - 文本输入、复选框、下拉选择
|
|
||||||
✓ **书签** - 持久化书签管理
|
|
||||||
✓ **历史** - 浏览历史记录
|
|
||||||
✓ **图片** - ASCII艺术渲染、智能缓存
|
|
||||||
✓ **性能** - LRU缓存、差分渲染、异步加载
|
|
||||||
|
|
||||||
---
|
---
|
||||||
更新时间: 2025-12-28
|
更新时间: 2025-12-27
|
||||||
|
|
|
||||||
146
TESTING.md
146
TESTING.md
|
|
@ -1,146 +0,0 @@
|
||||||
# TUT Browser Testing Guide
|
|
||||||
|
|
||||||
This document provides comprehensive testing instructions to ensure the browser works correctly.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the browser
|
|
||||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
|
||||||
cmake --build build
|
|
||||||
|
|
||||||
# Run with a test site
|
|
||||||
./build/tut http://example.com
|
|
||||||
|
|
||||||
# Or use the interactive test script
|
|
||||||
./test_browser.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Feature Testing Checklist
|
|
||||||
|
|
||||||
### Basic Navigation
|
|
||||||
- [ ] Browser loads and displays page correctly
|
|
||||||
- [ ] Scroll with j/k works smoothly
|
|
||||||
- [ ] Page up/down (Ctrl+d/u) works
|
|
||||||
- [ ] Go to top (gg) and bottom (G) works
|
|
||||||
- [ ] Back (h) and forward (l) navigation works
|
|
||||||
|
|
||||||
### Link Navigation
|
|
||||||
- [ ] Tab cycles through links
|
|
||||||
- [ ] Shift+Tab goes to previous link
|
|
||||||
- [ ] Enter follows the active link
|
|
||||||
- [ ] Links are highlighted when active
|
|
||||||
- [ ] Link URLs shown in status bar
|
|
||||||
|
|
||||||
### Search
|
|
||||||
- [ ] Press / to enter search mode
|
|
||||||
- [ ] Type search term and press Enter
|
|
||||||
- [ ] Matches are highlighted
|
|
||||||
- [ ] n/N navigate between matches
|
|
||||||
- [ ] Search count shown in status bar
|
|
||||||
|
|
||||||
### Form Interaction
|
|
||||||
- [ ] Press 'i' to focus first form field
|
|
||||||
- [ ] Tab/Shift+Tab navigate between fields
|
|
||||||
- [ ] Enter on text input enters edit mode
|
|
||||||
- [ ] Text can be typed and edited
|
|
||||||
- [ ] Backspace removes characters
|
|
||||||
- [ ] Enter or Esc exits edit mode
|
|
||||||
- [ ] Checkbox toggles with Enter
|
|
||||||
- [ ] SELECT dropdown shows options
|
|
||||||
- [ ] j/k navigate dropdown options
|
|
||||||
- [ ] Enter selects option in dropdown
|
|
||||||
- [ ] Selected option displays correctly
|
|
||||||
|
|
||||||
### Bookmarks
|
|
||||||
- [ ] Press B to bookmark current page
|
|
||||||
- [ ] Press D to remove bookmark
|
|
||||||
- [ ] Type :bookmarks to view all bookmarks
|
|
||||||
- [ ] Bookmarks persist between sessions
|
|
||||||
- [ ] Can click bookmarks to open pages
|
|
||||||
|
|
||||||
### History
|
|
||||||
- [ ] Type :history to view browsing history
|
|
||||||
- [ ] History shows URLs and titles
|
|
||||||
- [ ] History entries are clickable
|
|
||||||
- [ ] History persists between sessions
|
|
||||||
- [ ] Recent pages appear at top
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- [ ] Page loads are async with spinner
|
|
||||||
- [ ] Esc cancels page loading
|
|
||||||
- [ ] Page cache works (revisit loads instantly)
|
|
||||||
- [ ] Image cache works (images load from cache)
|
|
||||||
- [ ] Status shows "cached: N" for cached images
|
|
||||||
- [ ] Scrolling is smooth
|
|
||||||
- [ ] No noticeable lag in UI
|
|
||||||
|
|
||||||
### Commands
|
|
||||||
- [ ] :o URL opens new URL
|
|
||||||
- [ ] :q quits the browser
|
|
||||||
- [ ] :bookmarks shows bookmarks
|
|
||||||
- [ ] :history shows history
|
|
||||||
- [ ] :help shows help page
|
|
||||||
- [ ] ? also shows help page
|
|
||||||
|
|
||||||
### Edge Cases
|
|
||||||
- [ ] Window resize updates layout correctly
|
|
||||||
- [ ] Very long pages scroll correctly
|
|
||||||
- [ ] Pages without links/forms work
|
|
||||||
- [ ] Unicode text displays correctly
|
|
||||||
- [ ] CJK characters display correctly
|
|
||||||
- [ ] Images render as ASCII art (if stb_image available)
|
|
||||||
- [ ] Error handling for failed page loads
|
|
||||||
|
|
||||||
## Test Websites
|
|
||||||
|
|
||||||
### Simple Test Sites
|
|
||||||
1. **http://example.com** - Basic HTML test
|
|
||||||
2. **http://info.cern.ch** - First website ever, very simple
|
|
||||||
3. **http://motherfuckingwebsite.com** - Minimalist design
|
|
||||||
4. **http://textfiles.com** - Text-only content
|
|
||||||
|
|
||||||
### Form Testing
|
|
||||||
Create a local test file (test_form.html is provided):
|
|
||||||
```bash
|
|
||||||
python3 -m http.server 8000
|
|
||||||
./build/tut http://localhost:8000/test_form.html
|
|
||||||
```
|
|
||||||
|
|
||||||
Test the form features:
|
|
||||||
- Text input editing
|
|
||||||
- Checkbox toggling
|
|
||||||
- Dropdown selection
|
|
||||||
- Tab navigation
|
|
||||||
|
|
||||||
### Performance Testing
|
|
||||||
1. Load a page
|
|
||||||
2. Press 'r' to refresh (should use cache)
|
|
||||||
3. Load the same page again (should be instant from cache)
|
|
||||||
4. Check status bar shows "cached" messages
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
- HTTPS support depends on libcurl configuration
|
|
||||||
- Some complex JavaScript-heavy sites won't work (static HTML only)
|
|
||||||
- File:// URLs may not work depending on curl configuration
|
|
||||||
- Form submission is not yet implemented
|
|
||||||
- Cookies are not yet supported
|
|
||||||
|
|
||||||
## Reporting Issues
|
|
||||||
|
|
||||||
When reporting issues, please include:
|
|
||||||
1. The URL you were trying to load
|
|
||||||
2. The exact steps to reproduce
|
|
||||||
3. Expected vs actual behavior
|
|
||||||
4. Any error messages
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
The browser is working correctly if:
|
|
||||||
1. ✓ Can load and display simple HTML pages
|
|
||||||
2. ✓ Navigation (scroll, links) works smoothly
|
|
||||||
3. ✓ Form interaction is responsive and intuitive
|
|
||||||
4. ✓ Bookmarks and history persist correctly
|
|
||||||
5. ✓ Caching improves performance noticeably
|
|
||||||
6. ✓ No crashes or hangs during normal use
|
|
||||||
202
src/browser.cpp
202
src/browser.cpp
|
|
@ -42,18 +42,6 @@ struct CacheEntry {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 图片缓存条目
|
|
||||||
struct ImageCacheEntry {
|
|
||||||
tut::ImageData image_data;
|
|
||||||
std::chrono::steady_clock::time_point timestamp;
|
|
||||||
|
|
||||||
bool is_expired(int max_age_seconds = 600) const { // 图片缓存10分钟
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
|
||||||
auto age = std::chrono::duration_cast<std::chrono::seconds>(now - timestamp).count();
|
|
||||||
return age > max_age_seconds;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class Browser::Impl {
|
class Browser::Impl {
|
||||||
public:
|
public:
|
||||||
// 网络和解析
|
// 网络和解析
|
||||||
|
|
@ -97,11 +85,6 @@ public:
|
||||||
static constexpr int CACHE_MAX_AGE = 300; // 5分钟缓存
|
static constexpr int CACHE_MAX_AGE = 300; // 5分钟缓存
|
||||||
static constexpr size_t CACHE_MAX_SIZE = 20; // 最多缓存20个页面
|
static constexpr size_t CACHE_MAX_SIZE = 20; // 最多缓存20个页面
|
||||||
|
|
||||||
// 图片缓存
|
|
||||||
std::map<std::string, ImageCacheEntry> image_cache;
|
|
||||||
static constexpr int IMAGE_CACHE_MAX_AGE = 600; // 10分钟缓存
|
|
||||||
static constexpr size_t IMAGE_CACHE_MAX_SIZE = 100; // 最多缓存100张图片
|
|
||||||
|
|
||||||
// 异步加载状态
|
// 异步加载状态
|
||||||
LoadingState loading_state = LoadingState::IDLE;
|
LoadingState loading_state = LoadingState::IDLE;
|
||||||
std::string pending_url; // 正在加载的URL
|
std::string pending_url; // 正在加载的URL
|
||||||
|
|
@ -397,7 +380,6 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
int loaded = 0;
|
int loaded = 0;
|
||||||
int cached = 0;
|
|
||||||
int total = static_cast<int>(tree.images.size());
|
int total = static_cast<int>(tree.images.size());
|
||||||
|
|
||||||
for (DomNode* img_node : tree.images) {
|
for (DomNode* img_node : tree.images) {
|
||||||
|
|
@ -405,22 +387,9 @@ public:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded++;
|
|
||||||
|
|
||||||
// 检查缓存
|
|
||||||
auto cache_it = image_cache.find(img_node->img_src);
|
|
||||||
if (cache_it != image_cache.end() && !cache_it->second.is_expired(IMAGE_CACHE_MAX_AGE)) {
|
|
||||||
// 使用缓存的图片
|
|
||||||
img_node->image_data = cache_it->second.image_data;
|
|
||||||
cached++;
|
|
||||||
status_message = "🖼 Loading image " + std::to_string(loaded) + "/" + std::to_string(total) +
|
|
||||||
" (cached: " + std::to_string(cached) + ")";
|
|
||||||
draw_screen();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新状态
|
// 更新状态
|
||||||
status_message = "🖼 Downloading image " + std::to_string(loaded) + "/" + std::to_string(total) + "...";
|
loaded++;
|
||||||
|
status_message = "🖼 Loading image " + std::to_string(loaded) + "/" + std::to_string(total) + "...";
|
||||||
draw_screen();
|
draw_screen();
|
||||||
|
|
||||||
// 下载图片
|
// 下载图片
|
||||||
|
|
@ -432,32 +401,9 @@ public:
|
||||||
// 解码图片
|
// 解码图片
|
||||||
tut::ImageData img_data = tut::ImageRenderer::load_from_memory(response.data);
|
tut::ImageData img_data = tut::ImageRenderer::load_from_memory(response.data);
|
||||||
if (img_data.is_valid()) {
|
if (img_data.is_valid()) {
|
||||||
img_node->image_data = img_data;
|
img_node->image_data = std::move(img_data);
|
||||||
|
|
||||||
// 添加到缓存
|
|
||||||
// 限制缓存大小
|
|
||||||
if (image_cache.size() >= IMAGE_CACHE_MAX_SIZE) {
|
|
||||||
// 移除最老的缓存条目
|
|
||||||
auto oldest = image_cache.begin();
|
|
||||||
for (auto it = image_cache.begin(); it != image_cache.end(); ++it) {
|
|
||||||
if (it->second.timestamp < oldest->second.timestamp) {
|
|
||||||
oldest = it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
image_cache.erase(oldest);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageCacheEntry entry;
|
|
||||||
entry.image_data = std::move(img_data);
|
|
||||||
entry.timestamp = std::chrono::steady_clock::now();
|
|
||||||
image_cache[img_node->img_src] = std::move(entry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cached > 0) {
|
|
||||||
status_message = "✓ Loaded " + std::to_string(total) + " images (" +
|
|
||||||
std::to_string(cached) + " from cache)";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从URL中提取主机名
|
// 从URL中提取主机名
|
||||||
|
|
@ -510,8 +456,6 @@ public:
|
||||||
case InputMode::NORMAL: mode_str = "NORMAL"; break;
|
case InputMode::NORMAL: mode_str = "NORMAL"; break;
|
||||||
case InputMode::COMMAND:
|
case InputMode::COMMAND:
|
||||||
case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break;
|
case InputMode::SEARCH: mode_str = input_handler.get_buffer(); break;
|
||||||
case InputMode::FORM_EDIT: mode_str = "-- INSERT -- " + input_handler.get_buffer(); break;
|
|
||||||
case InputMode::SELECT_OPTION: mode_str = "-- SELECT --"; break;
|
|
||||||
default: mode_str = ""; break;
|
default: mode_str = ""; break;
|
||||||
}
|
}
|
||||||
framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
|
framebuffer->set_text(1, y, mode_str, colors::STATUSBAR_FG, colors::STATUSBAR_BG);
|
||||||
|
|
@ -596,29 +540,7 @@ public:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Action::FOLLOW_LINK:
|
case Action::FOLLOW_LINK:
|
||||||
// If on a form field, activate it instead of following link
|
if (active_link >= 0 && active_link < static_cast<int>(current_tree.links.size())) {
|
||||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field) {
|
|
||||||
if (field->input_type == "text" || field->input_type == "password") {
|
|
||||||
// Enter edit mode
|
|
||||||
input_handler.set_mode(InputMode::FORM_EDIT);
|
|
||||||
input_handler.set_buffer(field->value);
|
|
||||||
status_message = "-- INSERT --";
|
|
||||||
} else if (field->input_type == "checkbox") {
|
|
||||||
// Toggle checkbox
|
|
||||||
field->checked = !field->checked;
|
|
||||||
status_message = field->checked ? "☑ Checked" : "☐ Unchecked";
|
|
||||||
} else if (field->input_type == "select") {
|
|
||||||
// Enter dropdown selection mode
|
|
||||||
input_handler.set_mode(InputMode::SELECT_OPTION);
|
|
||||||
status_message = "-- SELECT -- (j/k to navigate, Enter to select, Esc to cancel)";
|
|
||||||
} else if (field->input_type == "submit" || field->element_type == ElementType::BUTTON) {
|
|
||||||
// TODO: Submit form
|
|
||||||
status_message = "Form submit (not yet implemented)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (active_link >= 0 && active_link < static_cast<int>(current_tree.links.size())) {
|
|
||||||
start_async_load(current_tree.links[active_link].url);
|
start_async_load(current_tree.links[active_link].url);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -687,97 +609,6 @@ public:
|
||||||
show_history();
|
show_history();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Action::NEXT_FIELD:
|
|
||||||
if (!current_tree.form_fields.empty()) {
|
|
||||||
// Save current text if in edit mode
|
|
||||||
if (input_handler.get_mode() == InputMode::FORM_EDIT &&
|
|
||||||
active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && (field->input_type == "text" || field->input_type == "password")) {
|
|
||||||
field->value = result.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next field
|
|
||||||
if (active_field < 0) {
|
|
||||||
active_field = 0; // First field
|
|
||||||
} else {
|
|
||||||
active_field = (active_field + 1) % current_tree.form_fields.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-scroll to field
|
|
||||||
// TODO: Implement scroll to field
|
|
||||||
status_message = "Field " + std::to_string(active_field + 1) + "/" +
|
|
||||||
std::to_string(current_tree.form_fields.size());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::PREV_FIELD:
|
|
||||||
if (!current_tree.form_fields.empty()) {
|
|
||||||
// Save current text if in edit mode
|
|
||||||
if (input_handler.get_mode() == InputMode::FORM_EDIT &&
|
|
||||||
active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && (field->input_type == "text" || field->input_type == "password")) {
|
|
||||||
field->value = result.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to previous field
|
|
||||||
if (active_field < 0) {
|
|
||||||
active_field = current_tree.form_fields.size() - 1; // Last field
|
|
||||||
} else {
|
|
||||||
active_field = (active_field - 1 + current_tree.form_fields.size()) %
|
|
||||||
current_tree.form_fields.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
status_message = "Field " + std::to_string(active_field + 1) + "/" +
|
|
||||||
std::to_string(current_tree.form_fields.size());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::EDIT_TEXT:
|
|
||||||
// Update field value in real-time
|
|
||||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && (field->input_type == "text" || field->input_type == "password")) {
|
|
||||||
field->value = result.text;
|
|
||||||
status_message = "Editing: " + result.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::NEXT_OPTION:
|
|
||||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
|
||||||
field->selected_option = (field->selected_option + 1) % field->options.size();
|
|
||||||
status_message = "Option: " + field->options[field->selected_option].second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::PREV_OPTION:
|
|
||||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
|
||||||
field->selected_option = (field->selected_option - 1 + field->options.size()) %
|
|
||||||
field->options.size();
|
|
||||||
status_message = "Option: " + field->options[field->selected_option].second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::SELECT_CURRENT_OPTION:
|
|
||||||
if (active_field >= 0 && active_field < static_cast<int>(current_tree.form_fields.size())) {
|
|
||||||
auto* field = current_tree.form_fields[active_field];
|
|
||||||
if (field && field->input_type == "select" && !field->options.empty()) {
|
|
||||||
field->value = field->options[field->selected_option].first;
|
|
||||||
status_message = "Selected: " + field->options[field->selected_option].second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action::QUIT:
|
case Action::QUIT:
|
||||||
break; // 在main loop处理
|
break; // 在main loop处理
|
||||||
|
|
||||||
|
|
@ -979,29 +810,8 @@ public:
|
||||||
|
|
||||||
<h2>Forms</h2>
|
<h2>Forms</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>i - Focus first form field</li>
|
<li>Tab - Navigate links and form fields</li>
|
||||||
<li>Tab/Shift+Tab - Navigate between fields</li>
|
<li>Enter - Activate link or submit form</li>
|
||||||
<li>Enter - Activate field (text input/checkbox/dropdown)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Text Input</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Type to edit text</li>
|
|
||||||
<li>Backspace to delete</li>
|
|
||||||
<li>Enter or Esc to finish editing</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Dropdown Selection</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Enter on SELECT to open options</li>
|
|
||||||
<li>j/k or arrows to navigate options</li>
|
|
||||||
<li>Enter to select, Esc to cancel</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Other</h2>
|
|
||||||
<ul>
|
|
||||||
<li>r - Refresh page (bypass cache)</li>
|
|
||||||
<li>Esc - Cancel loading</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
||||||
|
|
@ -370,29 +370,6 @@ std::unique_ptr<DomNode> DomTreeBuilder::convert_node(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For SELECT, collect all OPTION children
|
|
||||||
if (element.tag == GUMBO_TAG_SELECT) {
|
|
||||||
for (const auto& child : node->children) {
|
|
||||||
if (child->element_type == ElementType::OPTION) {
|
|
||||||
std::string option_value = child->value.empty() ? child->get_all_text() : child->value;
|
|
||||||
std::string option_text = child->get_all_text();
|
|
||||||
node->options.push_back({option_value, option_text});
|
|
||||||
|
|
||||||
// Set selected option if marked
|
|
||||||
if (child->checked) {
|
|
||||||
node->selected_option = node->options.size() - 1;
|
|
||||||
node->value = option_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default value to first option if no option is selected
|
|
||||||
if (!node->options.empty() && node->value.empty()) {
|
|
||||||
node->value = node->options[0].first;
|
|
||||||
node->selected_option = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset form ID if we are exiting a form
|
// Reset form ID if we are exiting a form
|
||||||
if (element.tag == GUMBO_TAG_FORM) {
|
if (element.tag == GUMBO_TAG_FORM) {
|
||||||
g_current_form_id = -1; // Assuming no nested forms
|
g_current_form_id = -1; // Assuming no nested forms
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,6 @@ struct DomNode {
|
||||||
bool checked = false;
|
bool checked = false;
|
||||||
int form_id = -1;
|
int form_id = -1;
|
||||||
|
|
||||||
// SELECT元素的选项
|
|
||||||
std::vector<std::pair<std::string, std::string>> options; // (value, text) pairs
|
|
||||||
int selected_option = 0; // 当前选中的选项索引
|
|
||||||
|
|
||||||
// 辅助方法
|
// 辅助方法
|
||||||
bool is_block_element() const;
|
bool is_block_element() const;
|
||||||
bool is_inline_element() const;
|
bool is_inline_element() const;
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,6 @@ public:
|
||||||
count_buffer.clear();
|
count_buffer.clear();
|
||||||
break;
|
break;
|
||||||
case '\t':
|
case '\t':
|
||||||
// Tab can navigate both links and fields - browser will decide
|
|
||||||
result.action = Action::NEXT_LINK;
|
result.action = Action::NEXT_LINK;
|
||||||
count_buffer.clear();
|
count_buffer.clear();
|
||||||
break;
|
break;
|
||||||
|
|
@ -136,7 +135,7 @@ public:
|
||||||
break;
|
break;
|
||||||
case '\n':
|
case '\n':
|
||||||
case '\r':
|
case '\r':
|
||||||
// Enter can follow links or activate fields - browser will decide
|
// If count buffer has a number, jump to that link
|
||||||
if (result.has_count) {
|
if (result.has_count) {
|
||||||
result.action = Action::GOTO_LINK;
|
result.action = Action::GOTO_LINK;
|
||||||
result.number = result.count;
|
result.number = result.count;
|
||||||
|
|
@ -145,11 +144,6 @@ public:
|
||||||
}
|
}
|
||||||
count_buffer.clear();
|
count_buffer.clear();
|
||||||
break;
|
break;
|
||||||
case 'i':
|
|
||||||
// 'i' to focus on first form field (like vim insert mode)
|
|
||||||
result.action = Action::NEXT_FIELD;
|
|
||||||
count_buffer.clear();
|
|
||||||
break;
|
|
||||||
case 'f':
|
case 'f':
|
||||||
// 'f' command: vimium-style link hints
|
// 'f' command: vimium-style link hints
|
||||||
result.action = Action::SHOW_LINK_HINTS;
|
result.action = Action::SHOW_LINK_HINTS;
|
||||||
|
|
@ -340,78 +334,6 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputResult process_form_edit_mode(int ch) {
|
|
||||||
InputResult result;
|
|
||||||
result.action = Action::NONE;
|
|
||||||
|
|
||||||
if (ch == 27) {
|
|
||||||
// ESC exits form edit mode
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
buffer.clear();
|
|
||||||
return result;
|
|
||||||
} else if (ch == '\n' || ch == '\r') {
|
|
||||||
// Enter submits the text
|
|
||||||
result.action = Action::EDIT_TEXT;
|
|
||||||
result.text = buffer;
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
buffer.clear();
|
|
||||||
return result;
|
|
||||||
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
|
||||||
// Backspace removes last character
|
|
||||||
if (!buffer.empty()) {
|
|
||||||
buffer.pop_back();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else if (ch == '\t') {
|
|
||||||
// Tab moves to next field while saving current text
|
|
||||||
result.action = Action::NEXT_FIELD;
|
|
||||||
result.text = buffer;
|
|
||||||
buffer.clear();
|
|
||||||
return result;
|
|
||||||
} else if (ch == KEY_BTAB) {
|
|
||||||
// Shift+Tab moves to previous field while saving current text
|
|
||||||
result.action = Action::PREV_FIELD;
|
|
||||||
result.text = buffer;
|
|
||||||
buffer.clear();
|
|
||||||
return result;
|
|
||||||
} else if (std::isprint(ch)) {
|
|
||||||
// Add printable characters to buffer
|
|
||||||
buffer += static_cast<char>(ch);
|
|
||||||
// Return EDIT_TEXT to update in real-time
|
|
||||||
result.action = Action::EDIT_TEXT;
|
|
||||||
result.text = buffer;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputResult process_select_option_mode(int ch) {
|
|
||||||
InputResult result;
|
|
||||||
result.action = Action::NONE;
|
|
||||||
|
|
||||||
if (ch == 27) {
|
|
||||||
// ESC cancels selection
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
return result;
|
|
||||||
} else if (ch == '\n' || ch == '\r') {
|
|
||||||
// Enter selects current option
|
|
||||||
result.action = Action::SELECT_CURRENT_OPTION;
|
|
||||||
mode = InputMode::NORMAL;
|
|
||||||
return result;
|
|
||||||
} else if (ch == 'j' || ch == KEY_DOWN) {
|
|
||||||
// Next option
|
|
||||||
result.action = Action::NEXT_OPTION;
|
|
||||||
return result;
|
|
||||||
} else if (ch == 'k' || ch == KEY_UP) {
|
|
||||||
// Previous option
|
|
||||||
result.action = Action::PREV_OPTION;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
InputHandler::InputHandler() : pImpl(std::make_unique<Impl>()) {}
|
||||||
|
|
@ -430,10 +352,6 @@ InputResult InputHandler::handle_key(int ch) {
|
||||||
return pImpl->process_link_mode(ch);
|
return pImpl->process_link_mode(ch);
|
||||||
case InputMode::LINK_HINTS:
|
case InputMode::LINK_HINTS:
|
||||||
return pImpl->process_link_hints_mode(ch);
|
return pImpl->process_link_hints_mode(ch);
|
||||||
case InputMode::FORM_EDIT:
|
|
||||||
return pImpl->process_form_edit_mode(ch);
|
|
||||||
case InputMode::SELECT_OPTION:
|
|
||||||
return pImpl->process_select_option_mode(ch);
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -457,14 +375,6 @@ void InputHandler::reset() {
|
||||||
pImpl->count_buffer.clear();
|
pImpl->count_buffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputHandler::set_mode(InputMode mode) {
|
|
||||||
pImpl->mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputHandler::set_buffer(const std::string& buffer) {
|
|
||||||
pImpl->buffer = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputHandler::set_status_callback(std::function<void(const std::string&)> callback) {
|
void InputHandler::set_status_callback(std::function<void(const std::string&)> callback) {
|
||||||
pImpl->status_callback = callback;
|
pImpl->status_callback = callback;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ enum class InputMode {
|
||||||
COMMAND,
|
COMMAND,
|
||||||
SEARCH,
|
SEARCH,
|
||||||
LINK,
|
LINK,
|
||||||
LINK_HINTS, // Vimium-style 'f' mode
|
LINK_HINTS // Vimium-style 'f' mode
|
||||||
FORM_EDIT, // Form field editing mode
|
|
||||||
SELECT_OPTION // Dropdown selection mode
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
|
|
@ -44,16 +42,7 @@ enum class Action {
|
||||||
ADD_BOOKMARK, // Add current page to bookmarks (B)
|
ADD_BOOKMARK, // Add current page to bookmarks (B)
|
||||||
REMOVE_BOOKMARK, // Remove current page from bookmarks (D)
|
REMOVE_BOOKMARK, // Remove current page from bookmarks (D)
|
||||||
SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks)
|
SHOW_BOOKMARKS, // Show bookmarks page (:bookmarks)
|
||||||
SHOW_HISTORY, // Show history page (:history)
|
SHOW_HISTORY // Show history page (:history)
|
||||||
NEXT_FIELD, // Move to next form field (Tab)
|
|
||||||
PREV_FIELD, // Move to previous form field (Shift+Tab)
|
|
||||||
ACTIVATE_FIELD, // Activate current field for editing (Enter)
|
|
||||||
TOGGLE_CHECKBOX, // Toggle checkbox state
|
|
||||||
EDIT_TEXT, // Edit text input (updates text buffer)
|
|
||||||
SUBMIT_FORM, // Submit form (Enter on submit button)
|
|
||||||
NEXT_OPTION, // Move to next dropdown option (j/down)
|
|
||||||
PREV_OPTION, // Move to previous dropdown option (k/up)
|
|
||||||
SELECT_CURRENT_OPTION // Select current dropdown option (Enter)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InputResult {
|
struct InputResult {
|
||||||
|
|
@ -73,8 +62,6 @@ public:
|
||||||
InputMode get_mode() const;
|
InputMode get_mode() const;
|
||||||
std::string get_buffer() const;
|
std::string get_buffer() const;
|
||||||
void reset();
|
void reset();
|
||||||
void set_mode(InputMode mode);
|
|
||||||
void set_buffer(const std::string& buffer);
|
|
||||||
void set_status_callback(std::function<void(const std::string&)> callback);
|
void set_status_callback(std::function<void(const std::string&)> callback);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
|
|
@ -432,13 +432,9 @@ void LayoutEngine::layout_form_element(const DomNode* node, Context& /*ctx*/, st
|
||||||
span.field_index = node->field_index;
|
span.field_index = node->field_index;
|
||||||
line.spans.push_back(span);
|
line.spans.push_back(span);
|
||||||
} else if (node->element_type == ElementType::SELECT) {
|
} else if (node->element_type == ElementType::SELECT) {
|
||||||
// 下拉选择 - 显示当前选中的选项
|
// 下拉选择
|
||||||
StyledSpan span;
|
StyledSpan span;
|
||||||
std::string selected_text = "Select";
|
span.text = "[▼ Select]";
|
||||||
if (node->selected_option >= 0 && node->selected_option < static_cast<int>(node->options.size())) {
|
|
||||||
selected_text = node->options[node->selected_option].second;
|
|
||||||
}
|
|
||||||
span.text = "[▼ " + selected_text + "]";
|
|
||||||
span.fg = colors::INPUT_FOCUS;
|
span.fg = colors::INPUT_FOCUS;
|
||||||
span.bg = colors::INPUT_BG;
|
span.bg = colors::INPUT_BG;
|
||||||
span.field_index = node->field_index;
|
span.field_index = node->field_index;
|
||||||
|
|
|
||||||
|
|
@ -125,14 +125,11 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
// 使用 ANSI 转义码清屏并移动光标到左上角
|
::clear();
|
||||||
std::printf("\033[2J\033[H");
|
|
||||||
std::fflush(stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
// ANSI 模式下不需要 ncurses 的 refresh,只需 flush
|
::refresh();
|
||||||
std::fflush(stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== True Color ====================
|
// ==================== True Color ====================
|
||||||
|
|
@ -226,20 +223,15 @@ public:
|
||||||
// ==================== 光标控制 ====================
|
// ==================== 光标控制 ====================
|
||||||
|
|
||||||
void move_cursor(int x, int y) {
|
void move_cursor(int x, int y) {
|
||||||
// 使用 ANSI 转义码移动光标(和 printf 输出兼容)
|
move(y, x); // ncurses使用 (y, x) 顺序
|
||||||
// 注意:ANSI 坐标是 1-indexed,所以需要 +1
|
|
||||||
std::printf("\033[%d;%dH", y + 1, x + 1);
|
|
||||||
std::fflush(stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void hide_cursor() {
|
void hide_cursor() {
|
||||||
std::printf("\033[?25l"); // DECTCEM: 隐藏光标
|
curs_set(0);
|
||||||
std::fflush(stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void show_cursor() {
|
void show_cursor() {
|
||||||
std::printf("\033[?25h"); // DECTCEM: 显示光标
|
curs_set(1);
|
||||||
std::fflush(stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 文本输出 ====================
|
// ==================== 文本输出 ====================
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# TUT Browser Interactive Test Script
|
|
||||||
# This script helps test the browser with various real websites
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo " TUT 2.0 Browser Interactive Testing"
|
|
||||||
echo "========================================"
|
|
||||||
echo ""
|
|
||||||
echo "This script will help you test the browser with real websites."
|
|
||||||
echo "Press Ctrl+C to exit at any time."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Build the browser
|
|
||||||
echo "Building the browser..."
|
|
||||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug > /dev/null 2>&1
|
|
||||||
cmake --build build > /dev/null 2>&1
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Build failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ Build successful!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test sites
|
|
||||||
declare -a sites=(
|
|
||||||
"http://example.com"
|
|
||||||
"http://info.cern.ch"
|
|
||||||
"http://motherfuckingwebsite.com"
|
|
||||||
"http://textfiles.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "Available test sites:"
|
|
||||||
echo "1. example.com - Simple static page"
|
|
||||||
echo "2. info.cern.ch - First website ever (historical)"
|
|
||||||
echo "3. motherfuckingwebsite.com - Minimalist design manifesto"
|
|
||||||
echo "4. textfiles.com - Text-only content"
|
|
||||||
echo "5. Custom URL"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
read -p "Select a site (1-5): " choice
|
|
||||||
|
|
||||||
case $choice in
|
|
||||||
1) url="${sites[0]}" ;;
|
|
||||||
2) url="${sites[1]}" ;;
|
|
||||||
3) url="${sites[2]}" ;;
|
|
||||||
4) url="${sites[3]}" ;;
|
|
||||||
5)
|
|
||||||
read -p "Enter URL (include http://): " url
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Invalid choice, using example.com"
|
|
||||||
url="${sites[0]}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================"
|
|
||||||
echo " Launching TUT Browser"
|
|
||||||
echo "========================================"
|
|
||||||
echo "URL: $url"
|
|
||||||
echo ""
|
|
||||||
echo "Keyboard shortcuts:"
|
|
||||||
echo " j/k - Scroll up/down"
|
|
||||||
echo " Tab - Next link/field"
|
|
||||||
echo " Enter - Follow link/activate field"
|
|
||||||
echo " i - Focus first form field"
|
|
||||||
echo " / - Search"
|
|
||||||
echo " h/l - Back/Forward"
|
|
||||||
echo " B - Bookmark"
|
|
||||||
echo " :o URL - Open URL"
|
|
||||||
echo " :q - Quit"
|
|
||||||
echo ""
|
|
||||||
read -p "Press Enter to start..."
|
|
||||||
|
|
||||||
# Launch the browser
|
|
||||||
./build/tut "$url"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Browser exited. Test complete!"
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Form Test</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Test Form</h1>
|
|
||||||
|
|
||||||
<form action="/submit" method="POST">
|
|
||||||
<p>Name: <input type="text" name="name" placeholder="Enter your name"></p>
|
|
||||||
<p>Email: <input type="text" name="email" placeholder="email@example.com"></p>
|
|
||||||
<p>Password: <input type="password" name="password"></p>
|
|
||||||
|
|
||||||
<p>Country: <select name="country">
|
|
||||||
<option value="">Select a country</option>
|
|
||||||
<option value="us">United States</option>
|
|
||||||
<option value="uk">United Kingdom</option>
|
|
||||||
<option value="ca">Canada</option>
|
|
||||||
<option value="au">Australia</option>
|
|
||||||
</select></p>
|
|
||||||
|
|
||||||
<p>Age Group: <select name="age">
|
|
||||||
<option value="18-25" selected>18-25</option>
|
|
||||||
<option value="26-35">26-35</option>
|
|
||||||
<option value="36-50">36-50</option>
|
|
||||||
<option value="51+">51+</option>
|
|
||||||
</select></p>
|
|
||||||
|
|
||||||
<p><input type="checkbox" name="subscribe"> Subscribe to newsletter</p>
|
|
||||||
<p><input type="checkbox" name="agree"> I agree to terms</p>
|
|
||||||
|
|
||||||
<p><input type="submit" value="Submit Form"></p>
|
|
||||||
<p><button>Click Me</button></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<h2>Links</h2>
|
|
||||||
<p><a href="https://example.com">Example Link</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in a new issue