]> git.gir.st - VimFx.git/blob - extension/packages/find-link.coffee
refactoring code
[VimFx.git] / extension / packages / find-link.coffee
1 { interfaces: Ci } = Components
2
3 HTMLDocument = Ci.nsIDOMHTMLDocument
4 XPathResult = Ci.nsIDOMXPathResult
5
6 # All the following elements qualify as a link
7 LINK_ELEMENTS = [
8 "a"
9 "area[@href]"
10 "button"
11 ]
12
13 # All elements that have one or more of the following properties
14 # qualify as a link
15 LINK_ELEMENT_PROPERTIES = [
16 "@onclick"
17 "@onmousedown"
18 "@onmouseup"
19 "@oncommand"
20 "@role='link'"
21 "@role='button'"
22 "contains(@class, 'button')"
23 ]
24
25 # Find a link with ref equals value
26 findLinkRef = (document, value) ->
27 relTags = ["link", "a", "area"]
28 for tag in relTags
29 elements = document.getElementsByTagName(tag)
30 for element in elements
31 if (element.hasAttribute("rel") && element.rel == value)
32 return element
33 null
34
35
36 # Find a link with match the patterns
37 findLinkPattern = (document, patterns) ->
38 links = getLinkElements(document)
39 candidateLinks = []
40
41 # at the end of this loop, candidateLinks will contain all visible links that match our patterns
42 # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
43 for i in [0...links.snapshotLength] by 1
44 link = links.snapshotItem(i)
45
46 if (isVisibleElement(link) &&
47 isElementMatchPattern(link, patterns))
48 candidateLinks.push(link)
49
50 return if (candidateLinks.length == 0)
51
52 for link in candidateLinks
53 link.wordCount = link.textContent.trim().split(/\s+/).length
54
55 # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
56 # in-page order of the links.
57 candidateLinks.forEach((a,i) -> a.originalIndex = i)
58
59 # favor shorter links, and ignore those that are more than one word longer than the shortest link
60 candidateLinks =
61 candidateLinks.sort((a, b) ->
62 if (a.wordCount == b.wordCount)
63 a.originalIndex - b.originalIndex
64 else
65 a.wordCount - b.wordCount
66 ).filter((a) ->
67 a.wordCount <= candidateLinks[0].wordCount + 1)
68
69 for pattern in patterns
70 exactWordRegex =
71 if /\b/.test(pattern[0]) or /\b/.test(pattern[pattern.length - 1])
72 new RegExp "\\b" + pattern + "\\b", "i"
73 else
74 new RegExp pattern, "i"
75
76 for candidateLink in candidateLinks
77 if (exactWordRegex.test(candidateLink.textContent))
78 return candidateLink
79 null
80
81
82 # Find a followable link match ref or patterns
83 find = (document, ref, patterns) ->
84 findLinkRef(document, ref) || findLinkPattern(document, patterns)
85
86
87 # Returns elements that qualify as links
88 # Generates and memoizes an XPath query internally
89 getLinkElements = do ->
90 # Some preparations done on startup
91 elements = [
92 LINK_ELEMENTS...
93 "*[#{ LINK_ELEMENT_PROPERTIES.join(' or ') }]"
94 ]
95
96 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
97 xpath = elements.reduce(reduce, []).join(' | ')
98
99 namespaceResolver = (namespace) ->
100 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
101
102 # The actual function that will return the desired elements
103 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
104 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
105
106
107 # Determine the link is visible
108 isVisibleElement = (element) ->
109 document = element.ownerDocument
110 window = document.defaultView
111
112 # element that isn't visible on the page
113 computedStyle = window.getComputedStyle(element, null)
114 if (computedStyle.getPropertyValue('visibility') != 'visible' ||
115 computedStyle.getPropertyValue('display') == 'none' ||
116 computedStyle.getPropertyValue('opacity') == '0')
117 return false
118
119 # element that has 0 dimension
120 clientRect = element.getBoundingClientRect()
121 if (clientRect.width == 0 || clientRect.height == 0)
122 return false
123
124 true
125
126
127 # Determine the link has a pattern matched
128 isElementMatchPattern = (element, patterns) ->
129 for pattern in patterns
130 if (element.textContent.toLowerCase().indexOf(pattern) != -1)
131 return true
132 false
133
134
135 exports.find = find
Imprint / Impressum