Initial commit

This commit is contained in:
2026-03-05 01:13:19 -06:00
commit 1ae223a1dc
21 changed files with 2404 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
package wiki
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const userAgent = "rsw-cli/1.0 (RuneScape Wiki CLI tool; https://github.com/runescape-wiki/rsw)"
// Client wraps HTTP requests to the MediaWiki API.
type Client struct {
baseURL string
httpClient *http.Client
}
// NewClient creates a wiki API client for the given base URL.
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 15 * time.Second,
},
}
}
// get performs a GET request with the given parameters and decodes JSON into dest.
func (c *Client) get(params url.Values, dest interface{}) error {
params.Set("format", "json")
reqURL := fmt.Sprintf("%s?%s", c.baseURL, params.Encode())
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
}

View File

@@ -0,0 +1,135 @@
package wiki
import (
"fmt"
"net/url"
"strconv"
)
// Section represents a section of a wiki page.
type Section struct {
Index string `json:"index"`
Level string `json:"level"`
Line string `json:"line"`
Number string `json:"number"`
Anchor string `json:"anchor"`
}
// ParsedPage contains the result of parsing a wiki page.
type ParsedPage struct {
Title string `json:"title"`
PageID int `json:"pageid"`
Wikitext string // Raw wikitext of the page or section
HTML string // Rendered HTML
Sections []Section // Table of contents
}
// parseResponse wraps the API response for action=parse.
type parseResponse struct {
Parse struct {
Title string `json:"title"`
PageID int `json:"pageid"`
Wikitext struct {
Content string `json:"*"`
} `json:"wikitext"`
Text struct {
Content string `json:"*"`
} `json:"text"`
Sections []Section `json:"sections"`
} `json:"parse"`
Error *struct {
Code string `json:"code"`
Info string `json:"info"`
} `json:"error"`
}
// GetPage fetches the full wikitext and section list for a page.
func (c *Client) GetPage(title string) (*ParsedPage, error) {
params := url.Values{
"action": {"parse"},
"page": {title},
"prop": {"wikitext|sections"},
"redirects": {"1"},
}
var resp parseResponse
if err := c.get(params, &resp); err != nil {
return nil, err
}
if resp.Error != nil {
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
}
return &ParsedPage{
Title: resp.Parse.Title,
PageID: resp.Parse.PageID,
Wikitext: resp.Parse.Wikitext.Content,
Sections: resp.Parse.Sections,
}, nil
}
// GetPageSection fetches the wikitext for a specific section of a page.
func (c *Client) GetPageSection(title string, sectionIndex int) (*ParsedPage, error) {
params := url.Values{
"action": {"parse"},
"page": {title},
"prop": {"wikitext"},
"section": {strconv.Itoa(sectionIndex)},
}
var resp parseResponse
if err := c.get(params, &resp); err != nil {
return nil, err
}
if resp.Error != nil {
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
}
return &ParsedPage{
Title: resp.Parse.Title,
PageID: resp.Parse.PageID,
Wikitext: resp.Parse.Wikitext.Content,
}, nil
}
// GetPageHTML fetches the rendered HTML for a page.
func (c *Client) GetPageHTML(title string) (*ParsedPage, error) {
params := url.Values{
"action": {"parse"},
"page": {title},
"prop": {"text|sections"},
}
var resp parseResponse
if err := c.get(params, &resp); err != nil {
return nil, err
}
if resp.Error != nil {
return nil, fmt.Errorf("wiki API error: %s — %s", resp.Error.Code, resp.Error.Info)
}
return &ParsedPage{
Title: resp.Parse.Title,
PageID: resp.Parse.PageID,
HTML: resp.Parse.Text.Content,
Sections: resp.Parse.Sections,
}, nil
}
// FindSectionIndex searches the section list for a section matching the given name.
// Returns -1 if not found.
func FindSectionIndex(sections []Section, name string) int {
for _, s := range sections {
if s.Line == name {
idx, err := strconv.Atoi(s.Index)
if err != nil {
continue
}
return idx
}
}
return -1
}

View File

@@ -0,0 +1,66 @@
package wiki
import (
"net/url"
"strconv"
"strings"
)
// SearchResult represents a single search hit from the wiki.
type SearchResult struct {
Title string `json:"title"`
Snippet string `json:"snippet"`
PageID int `json:"pageid"`
Size int `json:"size"`
}
// searchResponse is the raw API response shape for action=query&list=search.
type searchResponse struct {
Query struct {
Search []SearchResult `json:"search"`
} `json:"query"`
}
// Search performs a full-text search across wiki pages.
func (c *Client) Search(query string, limit int) ([]SearchResult, error) {
if limit <= 0 {
limit = 10
}
params := url.Values{
"action": {"query"},
"list": {"search"},
"srsearch": {query},
"srlimit": {strconv.Itoa(limit)},
"srprop": {"snippet|size"},
}
var resp searchResponse
if err := c.get(params, &resp); err != nil {
return nil, err
}
// Strip HTML tags from snippets (the API returns <span class="searchmatch">...</span>)
for i := range resp.Query.Search {
resp.Query.Search[i].Snippet = stripHTML(resp.Query.Search[i].Snippet)
}
return resp.Query.Search, nil
}
// stripHTML removes HTML tags from a string. Lightweight, no external dep.
func stripHTML(s string) string {
var b strings.Builder
inTag := false
for _, r := range s {
switch {
case r == '<':
inTag = true
case r == '>':
inTag = false
case !inTag:
b.WriteRune(r)
}
}
return b.String()
}