mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
package vuedocs
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
baseURL: "https://vuejs.org",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ParseReferencePage(html string, docURL string) (*Reference, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := &Reference{
|
||||
DocURL: docURL,
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
ref.GlobalAPI = p.extractGlobalAPI(doc, docURL)
|
||||
ref.Composition = p.extractCompositionAPI(doc, docURL)
|
||||
ref.Options = p.extractOptionsAPI(doc, docURL)
|
||||
ref.Directives = p.extractDirectives(doc, docURL)
|
||||
ref.Components = p.extractComponents(doc, docURL)
|
||||
ref.SpecialAttrs = p.extractSpecialAttrs(doc, docURL)
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func (p *Parser) ParseSearchResults(html string) ([]*SearchResult, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []*SearchResult
|
||||
|
||||
doc.Find(".search-result, a[href*='/api/'], .nav-link, .api-link").Each(func(i int, s *goquery.Selection) {
|
||||
result := &SearchResult{}
|
||||
|
||||
result.Name = strings.TrimSpace(s.Text())
|
||||
|
||||
if href, exists := s.Attr("href"); exists {
|
||||
result.DocURL = resolveURL(p.baseURL, href)
|
||||
|
||||
if strings.Contains(href, "/composition-") {
|
||||
result.Kind = "composition"
|
||||
} else if strings.Contains(href, "/options-") {
|
||||
result.Kind = "options"
|
||||
} else if strings.Contains(href, "/directive") {
|
||||
result.Kind = "directive"
|
||||
} else if strings.Contains(href, "/component") {
|
||||
result.Kind = "component"
|
||||
} else {
|
||||
result.Kind = "api"
|
||||
}
|
||||
}
|
||||
|
||||
if result.Name != "" {
|
||||
results = append(results, result)
|
||||
}
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (p *Parser) extractGlobalAPI(doc *goquery.Document, docURL string) []*API {
|
||||
var apis []*API
|
||||
|
||||
doc.Find("h2, h3, .api-item, [id]").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
globalAPIs := []string{"createApp", "createSSRApp", "defineComponent", "defineAsyncComponent", "resolveComponent", "resolveDirective", "withDirectives", "withKeys", "withModifiers"}
|
||||
isGlobal := false
|
||||
for _, name := range globalAPIs {
|
||||
if id == name || strings.Contains(text, name) {
|
||||
isGlobal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isGlobal {
|
||||
return
|
||||
}
|
||||
|
||||
api := &API{}
|
||||
|
||||
nameEl := s.Find("code, .name").First()
|
||||
if nameEl.Length() == 0 {
|
||||
nameEl = s
|
||||
}
|
||||
api.Name = strings.TrimSpace(nameEl.Text())
|
||||
api.Name = strings.TrimSuffix(api.Name, "(")
|
||||
|
||||
api.DocURL = docURL + "#" + api.Name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && api.Doc == "" {
|
||||
api.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
if api.Name != "" {
|
||||
apis = append(apis, api)
|
||||
}
|
||||
})
|
||||
|
||||
return apis
|
||||
}
|
||||
|
||||
func (p *Parser) extractCompositionAPI(doc *goquery.Document, docURL string) []*Composition {
|
||||
var compos []*Composition
|
||||
|
||||
doc.Find("h2, h3, h4, .api-item, [id^='ref'], [id^='reactive'], [id^='computed'], [id^='watch'], [id^='on']").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
if id == "" && !strings.Contains(text, "(") {
|
||||
return
|
||||
}
|
||||
|
||||
compNames := []string{"ref", "reactive", "computed", "watch", "watchEffect", "watchPostEffect", "watchSyncEffect", "onMounted", "onUpdated", "onUnmounted", "onBeforeMount", "onBeforeUpdate", "onBeforeUnmount", "onErrorCaptured", "onRenderTracked", "onRenderTriggered", "provide", "inject", "toRef", "toRefs", "toValue", "unref", "isRef", "shallowRef", "triggerRef", "customRef", "shallowReactive", "readonly", "shallowReadonly", "markRaw", "toRaw"}
|
||||
isComp := false
|
||||
for _, name := range compNames {
|
||||
if id == name || strings.HasPrefix(id, name+"-") || strings.Contains(text, name+"(") {
|
||||
isComp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isComp {
|
||||
return
|
||||
}
|
||||
|
||||
comp := &Composition{}
|
||||
|
||||
nameEl := s.Find("code, .name").First()
|
||||
if nameEl.Length() == 0 {
|
||||
nameEl = s
|
||||
}
|
||||
comp.Name = strings.TrimSpace(nameEl.Text())
|
||||
comp.Name = strings.TrimSuffix(comp.Name, "(")
|
||||
|
||||
if comp.Name == "" && id != "" {
|
||||
comp.Name = id
|
||||
}
|
||||
|
||||
comp.DocURL = docURL + "#" + comp.Name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && comp.Doc == "" {
|
||||
comp.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
if comp.Name != "" {
|
||||
compos = append(compos, comp)
|
||||
}
|
||||
})
|
||||
|
||||
return compos
|
||||
}
|
||||
|
||||
func (p *Parser) extractOptionsAPI(doc *goquery.Document, docURL string) []*Options {
|
||||
var opts []*Options
|
||||
|
||||
optionNames := []string{"data", "props", "computed", "methods", "watch", "emits", "expose", "setup", "name", "components", "directives", "inheritAttrs", "extends", "mixins", "provide", "inject", "template", "render", "renderTracked", "renderTriggered", "errorCaptured", "beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "beforeUnmount", "unmounted", "activated", "deactivated", "serverPrefetch"}
|
||||
|
||||
doc.Find("h2, h3, h4, .api-item").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
for _, name := range optionNames {
|
||||
if id == name || strings.Contains(strings.ToLower(text), name) {
|
||||
opt := &Options{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
opt.DocURL = docURL + "#" + name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && opt.Doc == "" {
|
||||
opt.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
opts = append(opts, opt)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func (p *Parser) extractDirectives(doc *goquery.Document, docURL string) []*Directive {
|
||||
var directives []*Directive
|
||||
|
||||
directiveNames := []string{"v-bind", "v-on", "v-model", "v-if", "v-else", "v-else-if", "v-show", "v-for", "v-slot", "v-text", "v-html", "v-cloak", "v-once", "v-pre", "v-memo"}
|
||||
|
||||
doc.Find("h2, h3, h4, .api-item").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
for _, name := range directiveNames {
|
||||
if id == name || strings.Contains(text, name) {
|
||||
dir := &Directive{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
dir.DocURL = docURL + "#" + name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && dir.Doc == "" {
|
||||
dir.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
directives = append(directives, dir)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return directives
|
||||
}
|
||||
|
||||
func (p *Parser) extractComponents(doc *goquery.Document, docURL string) []*Component {
|
||||
var components []*Component
|
||||
|
||||
componentNames := []string{"Transition", "TransitionGroup", "KeepAlive", "Teleport", "Suspense"}
|
||||
|
||||
doc.Find("h2, h3, h4, .api-item").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
for _, name := range componentNames {
|
||||
if id == name || strings.Contains(text, name) {
|
||||
comp := &Component{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
comp.DocURL = docURL + "#" + name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && comp.Doc == "" {
|
||||
comp.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
components = append(components, comp)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func (p *Parser) extractSpecialAttrs(doc *goquery.Document, docURL string) []*SpecialAttr {
|
||||
var attrs []*SpecialAttr
|
||||
|
||||
attrNames := []string{"key", "ref", "is"}
|
||||
|
||||
doc.Find("h2, h3, h4, .api-item").Each(func(_ int, s *goquery.Selection) {
|
||||
id, _ := s.Attr("id")
|
||||
text := s.Text()
|
||||
|
||||
for _, name := range attrNames {
|
||||
if id == name+"-attribute" || id == name || strings.Contains(text, name+" attribute") {
|
||||
attr := &SpecialAttr{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
attr.DocURL = docURL + "#" + name
|
||||
|
||||
next := s.Next()
|
||||
for next.Length() > 0 && !next.Is("h2, h3, h4") {
|
||||
if next.Is("p") && attr.Doc == "" {
|
||||
attr.Doc = strings.TrimSpace(next.Text())
|
||||
}
|
||||
next = next.Next()
|
||||
}
|
||||
|
||||
attrs = append(attrs, attr)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
func resolveURL(base string, href string) string {
|
||||
if strings.HasPrefix(href, "http") {
|
||||
return href
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return href
|
||||
}
|
||||
|
||||
hrefURL, err := url.Parse(href)
|
||||
if err != nil {
|
||||
return href
|
||||
}
|
||||
|
||||
return baseURL.ResolveReference(hrefURL).String()
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package vuedocs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
const testReferencePageHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Vue API Reference</h1>
|
||||
|
||||
<h2 id="ref">ref()</h2>
|
||||
<p>Takes an inner value and returns a reactive and mutable ref object.</p>
|
||||
<pre><code>function ref<T>(value: T): Ref<T></code></pre>
|
||||
|
||||
<h2 id="reactive">reactive()</h2>
|
||||
<p>Returns a reactive proxy of the object.</p>
|
||||
<pre><code>function reactive<T extends object>(target: T): T</code></pre>
|
||||
|
||||
<h2 id="computed">computed()</h2>
|
||||
<p>Takes a getter function and returns a readonly reactive ref object.</p>
|
||||
|
||||
<h3 id="v-bind">v-bind</h3>
|
||||
<p>Dynamically binds one or more attributes to an expression.</p>
|
||||
|
||||
<h3 id="Transition">Transition</h3>
|
||||
<p>Provides animated transition effects when elements enter or leave the DOM.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func TestParseReferencePage(t *testing.T) {
|
||||
parser := NewParser()
|
||||
ref, err := parser.ParseReferencePage(testReferencePageHTML, "https://vuejs.org/api/")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseReferencePage failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ref.Composition) == 0 {
|
||||
t.Error("Expected at least one composition API item")
|
||||
}
|
||||
|
||||
if len(ref.Directives) == 0 {
|
||||
t.Error("Expected at least one directive")
|
||||
}
|
||||
|
||||
if len(ref.Components) == 0 {
|
||||
t.Error("Expected at least one component")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractCompositionAPI(t *testing.T) {
|
||||
parser := NewParser()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(testReferencePageHTML))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse HTML: %v", err)
|
||||
}
|
||||
|
||||
compos := parser.extractCompositionAPI(doc, "https://vuejs.org/api/")
|
||||
|
||||
if len(compos) == 0 {
|
||||
t.Skip("No composition API items extracted from test HTML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDirectives(t *testing.T) {
|
||||
parser := NewParser()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(testReferencePageHTML))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse HTML: %v", err)
|
||||
}
|
||||
|
||||
directives := parser.extractDirectives(doc, "https://vuejs.org/api/")
|
||||
|
||||
found := false
|
||||
for _, d := range directives {
|
||||
if d.Name == "v-bind" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("Expected to find v-bind directive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
base string
|
||||
href string
|
||||
expected string
|
||||
}{
|
||||
{"https://vuejs.org", "/api/", "https://vuejs.org/api/"},
|
||||
{"https://vuejs.org", "https://example.com/page", "https://example.com/page"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.href, func(t *testing.T) {
|
||||
got := resolveURL(tt.base, tt.href)
|
||||
if got != tt.expected {
|
||||
t.Errorf("resolveURL(%q, %q) = %q, want %q", tt.base, tt.href, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Package vuedocs provides parsing and extraction for Vue.js documentation
|
||||
// from vuejs.org/api/.
|
||||
package vuedocs
|
||||
|
||||
import "time"
|
||||
|
||||
// Reference represents the Vue API reference.
|
||||
type Reference struct {
|
||||
Version string `json:"version"`
|
||||
DocURL string `json:"doc_url"`
|
||||
GlobalAPI []*API `json:"global_api,omitempty"`
|
||||
Composition []*Composition `json:"composition,omitempty"`
|
||||
Options []*Options `json:"options,omitempty"`
|
||||
Directives []*Directive `json:"directives,omitempty"`
|
||||
Components []*Component `json:"components,omitempty"`
|
||||
SpecialAttrs []*SpecialAttr `json:"special_attrs,omitempty"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// API represents a Vue global API.
|
||||
type API struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Parameters []*Parameter `json:"parameters,omitempty"`
|
||||
Returns string `json:"returns,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Composition represents a Composition API function.
|
||||
type Composition struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind,omitempty"` // ref, reactive, computed, watch, lifecycle
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Parameters []*Parameter `json:"parameters,omitempty"`
|
||||
Returns string `json:"returns,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Options represents an Options API property.
|
||||
type Options struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
Category string `json:"category,omitempty"` // state, lifecycle, rendering
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Directive represents a Vue directive.
|
||||
type Directive struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Usage string `json:"usage,omitempty"`
|
||||
Arguments []*Argument `json:"arguments,omitempty"`
|
||||
Modifiers []*Modifier `json:"modifiers,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Argument represents a directive argument.
|
||||
type Argument struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
}
|
||||
|
||||
// Modifier represents a directive modifier.
|
||||
type Modifier struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
}
|
||||
|
||||
// Component represents a Vue built-in component.
|
||||
type Component struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Props []*Prop `json:"props,omitempty"`
|
||||
Events []*Event `json:"events,omitempty"`
|
||||
Slots []*Slot `json:"slots,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Prop represents a component prop.
|
||||
type Prop struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Event represents a component event.
|
||||
type Event struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Parameters []*Parameter `json:"parameters,omitempty"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Slot represents a component slot.
|
||||
type Slot struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Props string `json:"props,omitempty"` // slot props type
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
|
||||
// SpecialAttr represents a special Vue attribute.
|
||||
type SpecialAttr struct {
|
||||
Name string `json:"name"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Usage string `json:"usage,omitempty"`
|
||||
Examples []*Example `json:"examples,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Deprecated string `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// Parameter represents a function parameter.
|
||||
type Parameter struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
Optional bool `json:"optional"`
|
||||
Default string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
// Example represents a code example.
|
||||
type Example struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
// SearchResult represents a search result.
|
||||
type SearchResult struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"` // api, composition, options, directive, component
|
||||
Doc string `json:"doc,omitempty"`
|
||||
DocURL string `json:"doc_url"`
|
||||
Score int `json:"score"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
}
|
||||
Reference in New Issue
Block a user