]> git.gir.st - VimFx.git/blob - extension/packages/find-link.coffee
fix to consistent styles
[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") and 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) and isElementMatchPattern(link, patterns)
47 candidateLinks.push(link)
48
49 return if candidateLinks.length == 0
50
51 for link in candidateLinks
52 link.wordCount = link.textContent.trim().split(/\s+/).length
53
54 # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
55 # in-page order of the links.
56 candidateLinks.forEach((a,i) -> a.originalIndex = i)
57
58 # favor shorter links, and ignore those that are more than one word longer than the shortest link
59 candidateLinks =
60 candidateLinks.sort((a, b) ->
61 if a.wordCount == b.wordCount
62 a.originalIndex - b.originalIndex
63 else
64 a.wordCount - b.wordCount
65 ).filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
66
67 for pattern in patterns
68 exactWordRegex =
69 if /\b/.test(pattern[0]) or /\b/.test(pattern[pattern.length - 1])
70 new RegExp '\\b' + pattern + '\\b', 'i'
71 else
72 new RegExp pattern, 'i'
73
74 for candidateLink in candidateLinks
75 if exactWordRegex.test(candidateLink.textContent)
76 return candidateLink
77 null
78
79
80 # Find a followable link match ref or patterns
81 find = (document, ref, patterns) ->
82 findLinkRef(document, ref) or findLinkPattern(document, patterns)
83
84
85 # Returns elements that qualify as links
86 # Generates and memoizes an XPath query internally
87 getLinkElements = do ->
88 # Some preparations done on startup
89 elements = [
90 LINK_ELEMENTS...
91 "*[#{ LINK_ELEMENT_PROPERTIES.join(' or ') }]"
92 ]
93
94 reduce = (m, rule) -> m.concat(["//#{ rule }", "//xhtml:#{ rule }"])
95 xpath = elements.reduce(reduce, []).join(' | ')
96
97 namespaceResolver = (namespace) ->
98 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
99
100 # The actual function that will return the desired elements
101 return (document, resultType = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) ->
102 return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null)
103
104
105 # Determine if the link is visible
106 isVisibleElement = (element) ->
107 document = element.ownerDocument
108 window = document.defaultView
109
110 # element that isn't visible on the page
111 computedStyle = window.getComputedStyle(element, null)
112 if computedStyle.getPropertyValue('visibility') != 'visible' or
113 computedStyle.getPropertyValue('display') == 'none' or
114 computedStyle.getPropertyValue('opacity') == '0'
115 return false
116
117 # element that has 0 dimension
118 clientRect = element.getBoundingClientRect()
119 if clientRect.width == 0 or clientRect.height == 0
120 return false
121
122 true
123
124
125 # Determine if the link has a pattern matched
126 isElementMatchPattern = (element, patterns) ->
127 for pattern in patterns
128 if element.textContent.toLowerCase().indexOf(pattern) != -1
129 return true
130 false
131
132
133 exports.find = find
Imprint / Impressum