require 'jsduck/util/html'
require 'jsduck/logger'
module JsDuck
module Inline
# Implementation of inline tag {@link}
#
# It also takes care of the auto-detection of links in text
# through the #create_magic_links method.
class Link
# Sets up instance to work in context of particular class, so
# that when {@link #blah} is encountered it knows that
# Context#blah is meant.
attr_accessor :class_context
# Sets up instance to work in context of particular doc object.
# Used for error reporting.
attr_accessor :doc_context
# JsDuck::Relations for looking up class names.
#
# When auto-creating class links from CamelCased names found from
# text, we check the relations object to see if a class with that
# name actually exists.
attr_accessor :relations
def initialize(opts={})
@class_context = ""
@doc_context = {}
@relations = {}
# Template HTML that replaces {@link Class#member anchor text}.
# Can contain placeholders:
#
# %c - full class name (e.g. "Ext.Panel")
# %m - class member name prefixed with member type (e.g. "method-urlEncode")
# %# - inserts "#" if member name present
# %- - inserts "-" if member name present
# %a - anchor text for link
@tpl = opts[:link_tpl] || '%a'
@re = /\{@link\s+(\S*?)(?:\s+(.+?))?\}/m
end
# Takes StringScanner instance.
#
# Looks for inline tag at the current scan pointer position, when
# found, moves scan pointer forward and performs the apporpriate
# replacement.
def replace(input)
if input.check(@re)
input.scan(@re).sub(@re) { apply_tpl($1, $2, $&) }
else
false
end
end
# applies the link template
def apply_tpl(target, text, full_link)
if target =~ /^(.*)#(static-)?(?:(cfg|property|method|event|css_var|css_mixin)-)?(.*)$/
cls = $1.empty? ? @class_context : $1
static = $2 ? true : nil
type = $3 ? $3.intern : nil
member = $4
else
cls = target
static = nil
type = false
member = false
end
# Construct link text
if text
text = text
elsif member
text = (cls == @class_context) ? member : (cls + "." + member)
else
text = cls
end
file = @doc_context[:filename]
line = @doc_context[:linenr]
if !@relations[cls]
Logger.warn(:link, "#{full_link} links to non-existing class", file, line)
return text
elsif member
ms = find_members(cls, {:name => member, :tagname => type, :static => static})
if ms.length == 0
Logger.warn(:link, "#{full_link} links to non-existing member", file, line)
return text
end
if ms.length > 1
# When multiple public members, see if there remains just
# one when we ignore the static members. If there's more,
# report ambiguity. If there's only static members, also
# report ambiguity.
instance_ms = ms.find_all {|m| !m[:meta][:static] }
if instance_ms.length > 1
alternatives = instance_ms.map {|m| "#{m[:tagname]} in #{m[:owner]}" }.join(", ")
Logger.warn(:link_ambiguous, "#{full_link} is ambiguous: "+alternatives, file, line)
elsif instance_ms.length == 0
static_ms = ms.find_all {|m| m[:meta][:static] }
alternatives = static_ms.map {|m| "static " + m[:tagname].to_s }.join(", ")
Logger.warn(:link_ambiguous, "#{full_link} is ambiguous: "+alternatives, file, line)
end
end
return link(cls, member, text, type, static)
else
return link(cls, false, text)
end
end
# Looks input text for patterns like:
#
# My.ClassName
# MyClass#method
# #someProperty
#
# and converts them to links, as if they were surrounded with
# {@link} tag. One notable exception is that Foo is not created to
# link, even when Foo class exists, but Foo.Bar is. This is to
# avoid turning normal words into links. For example:
#
# Math involves a lot of numbers. Ext JS is a JavaScript framework.
#
# In these sentences we don't want to link "Math" and "Ext" to the
# corresponding JS classes. And that's why we auto-link only
# class names containing a dot "."
#
def create_magic_links(input)
cls_re = "([A-Z][A-Za-z0-9.]*[A-Za-z0-9])"
member_re = "(?:#([A-Za-z0-9]+))"
input.gsub(/\b#{cls_re}#{member_re}?\b|#{member_re}\b/m) do
replace_magic_link($1, $2 || $3)
end
end
def replace_magic_link(cls, member)
if cls && member
if @relations[cls] && get_matching_member(cls, {:name => member})
return link(cls, member, cls+"."+member)
else
warn_magic_link("#{cls}##{member} links to non-existing " + (@relations[cls] ? "member" : "class"))
end
elsif cls && cls =~ /\./
if @relations[cls]
return link(cls, nil, cls)
else
cls2, member2 = split_to_cls_and_member(cls)
if @relations[cls2] && get_matching_member(cls2, {:name => member2})
return link(cls2, member2, cls2+"."+member2)
elsif cls =~ /\.(js|css|html|php)\Z/
# Ignore common filenames
else
warn_magic_link("#{cls} links to non-existing class")
end
end
elsif !cls && member
if get_matching_member(@class_context, {:name => member})
return link(@class_context, member, member)
elsif member =~ /\A([A-F0-9]{3}|[A-F0-9]{6})\Z/i || member =~ /\A[0-9]/
# Ignore HEX color codes and
# member names beginning with number
else
warn_magic_link("##{member} links to non-existing member")
end
end
return "#{cls}#{member ? '#' : ''}#{member}"
end
def split_to_cls_and_member(str)
parts = str.split(/\./)
return [parts.slice(0, parts.length-1).join("."), parts.last]
end
def warn_magic_link(msg)
Logger.warn(:link_auto, msg, @doc_context[:filename], @doc_context[:linenr])
end
# applies the link template
def link(cls, member, anchor_text, type=nil, static=nil)
# Use the canonical class name for link (not some alternateClassName)
cls = @relations[cls][:name]
# prepend type name to member name
member = member && get_matching_member(cls, {:name => member, :tagname => type, :static => static})
@tpl.gsub(/(%[\w#-])/) do
case $1
when '%c'
cls
when '%m'
member ? member[:id] : ""
when '%#'
member ? "#" : ""
when '%-'
member ? "-" : ""
when '%a'
Util::HTML.escape(anchor_text||"")
else
$1
end
end
end
def get_matching_member(cls, query)
ms = find_members(cls, query)
if ms.length > 1
instance_ms = ms.find_all {|m| !m[:meta][:static] }
instance_ms.length > 0 ? instance_ms[0] : ms.find_all {|m| m[:meta][:static] }[0]
else
ms[0]
end
end
def find_members(cls, query)
@relations[cls] ? @relations[cls].find_members(query) : []
end
end
end
end